1 /*
<lambda>null2 * Copyright 2022 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 package androidx.graphics.shapes
18
19 import androidx.annotation.IntRange
20 import androidx.collection.MutableFloatList
21 import kotlin.jvm.JvmOverloads
22 import kotlin.math.abs
23 import kotlin.math.max
24 import kotlin.math.min
25 import kotlin.math.sqrt
26
27 /**
28 * The RoundedPolygon class allows simple construction of polygonal shapes with optional rounding at
29 * the vertices. Polygons can be constructed with either the number of vertices desired or an
30 * ordered list of vertices.
31 */
32 class RoundedPolygon internal constructor(val features: List<Feature>, internal val center: Point) {
33 val centerX
34 get() = center.x
35
36 val centerY
37 get() = center.y
38
39 /** A flattened version of the [Feature]s, as a List<Cubic>. */
40 val cubics = buildList {
41 // The first/last mechanism here ensures that the final anchor point in the shape
42 // exactly matches the first anchor point. There can be rendering artifacts introduced
43 // by those points being slightly off, even by much less than a pixel
44 var firstCubic: Cubic? = null
45 var lastCubic: Cubic? = null
46 var firstFeatureSplitStart: List<Cubic>? = null
47 var firstFeatureSplitEnd: List<Cubic>? = null
48 if (features.size > 0 && features[0].cubics.size == 3) {
49 val centerCubic = features[0].cubics[1]
50 val (start, end) = centerCubic.split(.5f)
51 firstFeatureSplitStart = mutableListOf(features[0].cubics[0], start)
52 firstFeatureSplitEnd = mutableListOf(end, features[0].cubics[2])
53 }
54 // iterating one past the features list size allows us to insert the initial split
55 // cubic if it exists
56 for (i in 0..features.size) {
57 val featureCubics =
58 if (i == 0 && firstFeatureSplitEnd != null) firstFeatureSplitEnd
59 else if (i == features.size) {
60 if (firstFeatureSplitStart != null) firstFeatureSplitStart else break
61 } else features[i].cubics
62 for (j in featureCubics.indices) {
63 // Skip zero-length curves; they add nothing and can trigger rendering artifacts
64 val cubic = featureCubics[j]
65 if (!cubic.zeroLength()) {
66 if (lastCubic != null) add(lastCubic)
67 lastCubic = cubic
68 if (firstCubic == null) firstCubic = cubic
69 } else {
70 if (lastCubic != null) {
71 // Dropping several zero-ish length curves in a row can lead to
72 // enough discontinuity to throw an exception later, even though the
73 // distances are quite small. Account for that by making the last
74 // cubic use the latest anchor point, always.
75 lastCubic = Cubic(lastCubic.points.copyOf()) // Make a copy before mutating
76 lastCubic.points[6] = cubic.anchor1X
77 lastCubic.points[7] = cubic.anchor1Y
78 }
79 }
80 }
81 }
82 if (lastCubic != null && firstCubic != null) {
83 add(
84 Cubic(
85 lastCubic.anchor0X,
86 lastCubic.anchor0Y,
87 lastCubic.control0X,
88 lastCubic.control0Y,
89 lastCubic.control1X,
90 lastCubic.control1Y,
91 firstCubic.anchor0X,
92 firstCubic.anchor0Y
93 )
94 )
95 } else {
96 // Empty / 0-sized polygon.
97 add(
98 Cubic(
99 centerX,
100 centerY,
101 centerX,
102 centerY,
103 centerX,
104 centerY,
105 centerX,
106 centerY,
107 )
108 )
109 }
110 }
111
112 init {
113 var prevCubic = cubics[cubics.size - 1]
114 debugLog("RoundedPolygon") { "Cubic-1 = $prevCubic" }
115 for (index in cubics.indices) {
116 val cubic = cubics[index]
117 debugLog("RoundedPolygon") { "Cubic = $cubic" }
118 if (
119 abs(cubic.anchor0X - prevCubic.anchor1X) > DistanceEpsilon ||
120 abs(cubic.anchor0Y - prevCubic.anchor1Y) > DistanceEpsilon
121 ) {
122 debugLog("RoundedPolygon") {
123 "Ix: $index | (${cubic.anchor0X},${cubic.anchor0Y}) vs $prevCubic"
124 }
125 throw IllegalArgumentException(
126 "RoundedPolygon must be contiguous, with the anchor points of all curves " +
127 "matching the anchor points of the preceding and succeeding cubics"
128 )
129 }
130 prevCubic = cubic
131 }
132 }
133
134 /**
135 * Transforms (scales/translates/etc.) this [RoundedPolygon] with the given [PointTransformer]
136 * and returns a new [RoundedPolygon]. This is a low level API and there should be more platform
137 * idiomatic ways to transform a [RoundedPolygon] provided by the platform specific wrapper.
138 *
139 * @param f The [PointTransformer] used to transform this [RoundedPolygon]
140 */
141 fun transformed(f: PointTransformer): RoundedPolygon {
142 val center = center.transformed(f)
143 return RoundedPolygon(
144 buildList {
145 for (i in features.indices) {
146 add(features[i].transformed(f))
147 }
148 },
149 center
150 )
151 }
152
153 /**
154 * Creates a new RoundedPolygon, moving and resizing this one, so it's completely inside the
155 * (0, 0) -> (1, 1) square, centered if there extra space in one direction
156 */
157 fun normalized(): RoundedPolygon {
158 val bounds = calculateBounds()
159 val width = bounds[2] - bounds[0]
160 val height = bounds[3] - bounds[1]
161 val side = max(width, height)
162 // Center the shape if bounds are not a square
163 val offsetX = (side - width) / 2 - bounds[0] /* left */
164 val offsetY = (side - height) / 2 - bounds[1] /* top */
165 return transformed { x, y -> TransformResult((x + offsetX) / side, (y + offsetY) / side) }
166 }
167
168 override fun toString(): String =
169 "[RoundedPolygon." +
170 " Cubics = " +
171 cubics.joinToString() +
172 " || Features = " +
173 features.joinToString() +
174 " || Center = ($centerX, $centerY)]"
175
176 /**
177 * Like [calculateBounds], this function calculates the axis-aligned bounds of the object and
178 * returns that rectangle. But this function determines the max dimension of the shape (by
179 * calculating the distance from its center to the start and midpoint of each curve) and returns
180 * a square which can be used to hold the object in any rotation. This function can be used, for
181 * example, to calculate the max size of a UI element meant to hold this shape in any rotation.
182 *
183 * @param bounds a buffer to hold the results. If not supplied, a temporary buffer will be
184 * created.
185 * @return The axis-aligned max bounding box for this object, where the rectangles left, top,
186 * right, and bottom values will be stored in entries 0, 1, 2, and 3, in that order.
187 */
188 fun calculateMaxBounds(bounds: FloatArray = FloatArray(4)): FloatArray {
189 require(bounds.size >= 4) { "Required bounds size of 4" }
190 var maxDistSquared = 0f
191 for (i in cubics.indices) {
192 val cubic = cubics[i]
193 val anchorDistance = distanceSquared(cubic.anchor0X - centerX, cubic.anchor0Y - centerY)
194 val middlePoint = cubic.pointOnCurve(.5f)
195 val middleDistance = distanceSquared(middlePoint.x - centerX, middlePoint.y - centerY)
196 maxDistSquared = max(maxDistSquared, max(anchorDistance, middleDistance))
197 }
198 val distance = sqrt(maxDistSquared)
199 bounds[0] = centerX - distance
200 bounds[1] = centerY - distance
201 bounds[2] = centerX + distance
202 bounds[3] = centerY + distance
203 return bounds
204 }
205
206 /**
207 * Calculates the axis-aligned bounds of the object.
208 *
209 * @param approximate when true, uses a faster calculation to create the bounding box based on
210 * the min/max values of all anchor and control points that make up the shape. Default value
211 * is true.
212 * @param bounds a buffer to hold the results. If not supplied, a temporary buffer will be
213 * created.
214 * @return The axis-aligned bounding box for this object, where the rectangles left, top, right,
215 * and bottom values will be stored in entries 0, 1, 2, and 3, in that order.
216 */
217 @JvmOverloads
218 fun calculateBounds(
219 bounds: FloatArray = FloatArray(4),
220 approximate: Boolean = true
221 ): FloatArray {
222 require(bounds.size >= 4) { "Required bounds size of 4" }
223 var minX = Float.MAX_VALUE
224 var minY = Float.MAX_VALUE
225 var maxX = Float.MIN_VALUE
226 var maxY = Float.MIN_VALUE
227 for (i in cubics.indices) {
228 val cubic = cubics[i]
229 cubic.calculateBounds(bounds, approximate = approximate)
230 minX = min(minX, bounds[0])
231 minY = min(minY, bounds[1])
232 maxX = max(maxX, bounds[2])
233 maxY = max(maxY, bounds[3])
234 }
235 bounds[0] = minX
236 bounds[1] = minY
237 bounds[2] = maxX
238 bounds[3] = maxY
239 return bounds
240 }
241
242 companion object {}
243
244 override fun equals(other: Any?): Boolean {
245 if (this === other) return true
246 if (other !is RoundedPolygon) return false
247
248 return features == other.features
249 }
250
251 override fun hashCode(): Int {
252 return features.hashCode()
253 }
254 }
255
256 /**
257 * This constructor takes the number of vertices in the resulting polygon. These vertices are
258 * positioned on a virtual circle around a given center with each vertex positioned [radius]
259 * distance from that center, equally spaced (with equal angles between them). If no radius is
260 * supplied, the shape will be created with a default radius of 1, resulting in a shape whose
261 * vertices lie on a unit circle, with width/height of 2. That default polygon will probably need to
262 * be rescaled using [transformed] into the appropriate size for the UI in which it will be drawn.
263 *
264 * The [rounding] and [perVertexRounding] parameters are optional. If not supplied, the result will
265 * be a regular polygon with straight edges and unrounded corners.
266 *
267 * @param numVertices The number of vertices in this polygon.
268 * @param radius The radius of the polygon, in pixels. This radius determines the initial size of
269 * the object, but it can be transformed later by using the [transformed] function.
270 * @param centerX The X coordinate of the center of the polygon, around which all vertices will be
271 * placed. The default center is at (0,0).
272 * @param centerY The Y coordinate of the center of the polygon, around which all vertices will be
273 * placed. The default center is at (0,0).
274 * @param rounding The [CornerRounding] properties of all vertices. If some vertices should have
275 * different rounding properties, then use [perVertexRounding] instead. The default rounding value
276 * is [CornerRounding.Unrounded], meaning that the polygon will use the vertices themselves in the
277 * final shape and not curves rounded around the vertices.
278 * @param perVertexRounding The [CornerRounding] properties of every vertex. If this parameter is
279 * not null, then it must have [numVertices] elements. If this parameter is null, then the polygon
280 * will use the [rounding] parameter for every vertex instead. The default value is null.
281 * @throws IllegalArgumentException If [perVertexRounding] is not null and its size is not equal to
282 * [numVertices].
283 * @throws IllegalArgumentException [numVertices] must be at least 3.
284 */
285 @JvmOverloads
RoundedPolygonnull286 fun RoundedPolygon(
287 @IntRange(from = 3) numVertices: Int,
288 radius: Float = 1f,
289 centerX: Float = 0f,
290 centerY: Float = 0f,
291 rounding: CornerRounding = CornerRounding.Unrounded,
292 perVertexRounding: List<CornerRounding>? = null
293 ) =
294 RoundedPolygon(
295 verticesFromNumVerts(numVertices, radius, centerX, centerY),
296 rounding = rounding,
297 perVertexRounding = perVertexRounding,
298 centerX = centerX,
299 centerY = centerY
300 )
301
302 /** Creates a copy of the given [RoundedPolygon] */
303 fun RoundedPolygon(source: RoundedPolygon) = RoundedPolygon(source.features, source.center)
304
305 /**
306 * This function takes the vertices (either supplied or calculated, depending on the constructor
307 * called), plus [CornerRounding] parameters, and creates the actual [RoundedPolygon] shape,
308 * rounding around the vertices (or not) as specified. The result is a list of [Cubic] curves which
309 * represent the geometry of the final shape.
310 *
311 * @param vertices The list of vertices in this polygon specified as pairs of x/y coordinates in
312 * this FloatArray. This should be an ordered list (with the outline of the shape going from each
313 * vertex to the next in order of this list), otherwise the results will be undefined.
314 * @param rounding The [CornerRounding] properties of all vertices. If some vertices should have
315 * different rounding properties, then use [perVertexRounding] instead. The default rounding value
316 * is [CornerRounding.Unrounded], meaning that the polygon will use the vertices themselves in the
317 * final shape and not curves rounded around the vertices.
318 * @param perVertexRounding The [CornerRounding] properties of all vertices. If this parameter is
319 * not null, then it must have the same size as [vertices]. If this parameter is null, then the
320 * polygon will use the [rounding] parameter for every vertex instead. The default value is null.
321 * @param centerX The X coordinate of the center of the polygon, around which all vertices will be
322 * placed. The default center is at (0,0).
323 * @param centerY The Y coordinate of the center of the polygon, around which all vertices will be
324 * placed. The default center is at (0,0).
325 * @throws IllegalArgumentException if the number of vertices is less than 3 (the [vertices]
326 * parameter has less than 6 Floats). Or if the [perVertexRounding] parameter is not null and the
327 * size doesn't match the number vertices.
328 */
329 // TODO(performance): Update the map calls to more efficient code that doesn't allocate Iterators
330 // unnecessarily.
331 @JvmOverloads
332 fun RoundedPolygon(
333 vertices: FloatArray,
334 rounding: CornerRounding = CornerRounding.Unrounded,
335 perVertexRounding: List<CornerRounding>? = null,
336 centerX: Float = Float.MIN_VALUE,
337 centerY: Float = Float.MIN_VALUE
338 ): RoundedPolygon {
339 if (vertices.size < 6) {
340 throw IllegalArgumentException("Polygons must have at least 3 vertices")
341 }
342 if (vertices.size % 2 == 1) {
343 throw IllegalArgumentException("The vertices array should have even size")
344 }
345 if (perVertexRounding != null && perVertexRounding.size * 2 != vertices.size) {
346 throw IllegalArgumentException(
347 "perVertexRounding list should be either null or " +
348 "the same size as the number of vertices (vertices.size / 2)"
349 )
350 }
351 val corners = mutableListOf<List<Cubic>>()
352 val n = vertices.size / 2
353 val roundedCorners = mutableListOf<RoundedCorner>()
354 for (i in 0 until n) {
355 val vtxRounding = perVertexRounding?.get(i) ?: rounding
356 val prevIndex = ((i + n - 1) % n) * 2
357 val nextIndex = ((i + 1) % n) * 2
358 roundedCorners.add(
359 RoundedCorner(
360 Point(vertices[prevIndex], vertices[prevIndex + 1]),
361 Point(vertices[i * 2], vertices[i * 2 + 1]),
362 Point(vertices[nextIndex], vertices[nextIndex + 1]),
363 vtxRounding
364 )
365 )
366 }
367
368 // For each side, check if we have enough space to do the cuts needed, and if not split
369 // the available space, first for round cuts, then for smoothing if there is space left.
370 // Each element in this list is a pair, that represent how much we can do of the cut for
371 // the given side (side i goes from corner i to corner i+1), the elements of the pair are:
372 // first is how much we can use of expectedRoundCut, second how much of expectedCut
373 val cutAdjusts =
374 (0 until n).map { ix ->
375 val expectedRoundCut =
376 roundedCorners[ix].expectedRoundCut + roundedCorners[(ix + 1) % n].expectedRoundCut
377 val expectedCut =
378 roundedCorners[ix].expectedCut + roundedCorners[(ix + 1) % n].expectedCut
379 val vtxX = vertices[ix * 2]
380 val vtxY = vertices[ix * 2 + 1]
381 val nextVtxX = vertices[((ix + 1) % n) * 2]
382 val nextVtxY = vertices[((ix + 1) % n) * 2 + 1]
383 val sideSize = distance(vtxX - nextVtxX, vtxY - nextVtxY)
384
385 // Check expectedRoundCut first, and ensure we fulfill rounding needs first for
386 // both corners before using space for smoothing
387 if (expectedRoundCut > sideSize) {
388 // Not enough room for fully rounding, see how much we can actually do.
389 sideSize / expectedRoundCut to 0f
390 } else if (expectedCut > sideSize) {
391 // We can do full rounding, but not full smoothing.
392 1f to (sideSize - expectedRoundCut) / (expectedCut - expectedRoundCut)
393 } else {
394 // There is enough room for rounding & smoothing.
395 1f to 1f
396 }
397 }
398 // Create and store list of beziers for each [potentially] rounded corner
399 for (i in 0 until n) {
400 // allowedCuts[0] is for the side from the previous corner to this one,
401 // allowedCuts[1] is for the side from this corner to the next one.
402 val allowedCuts = MutableFloatList(2)
403 for (delta in 0..1) {
404 val (roundCutRatio, cutRatio) = cutAdjusts[(i + n - 1 + delta) % n]
405 allowedCuts.add(
406 roundedCorners[i].expectedRoundCut * roundCutRatio +
407 (roundedCorners[i].expectedCut - roundedCorners[i].expectedRoundCut) * cutRatio
408 )
409 }
410 corners.add(
411 roundedCorners[i].getCubics(allowedCut0 = allowedCuts[0], allowedCut1 = allowedCuts[1])
412 )
413 }
414 // Finally, store the calculated cubics. This includes all of the rounded corners
415 // from above, along with new cubics representing the edges between those corners.
416 val tempFeatures = mutableListOf<Feature>()
417 for (i in 0 until n) {
418 // Note that these indices are for pairs of values (points), they need to be
419 // doubled to access the xy values in the vertices float array
420 val prevVtxIndex = (i + n - 1) % n
421 val nextVtxIndex = (i + 1) % n
422 val currVertex = Point(vertices[i * 2], vertices[i * 2 + 1])
423 val prevVertex = Point(vertices[prevVtxIndex * 2], vertices[prevVtxIndex * 2 + 1])
424 val nextVertex = Point(vertices[nextVtxIndex * 2], vertices[nextVtxIndex * 2 + 1])
425 val convex = convex(prevVertex, currVertex, nextVertex)
426 tempFeatures.add(Feature.Corner(corners[i], convex))
427 tempFeatures.add(
428 Feature.Edge(
429 listOf(
430 Cubic.straightLine(
431 corners[i].last().anchor1X,
432 corners[i].last().anchor1Y,
433 corners[(i + 1) % n].first().anchor0X,
434 corners[(i + 1) % n].first().anchor0Y
435 )
436 )
437 )
438 )
439 }
440
441 val (cx, cy) =
442 if (centerX == Float.MIN_VALUE || centerY == Float.MIN_VALUE) {
443 calculateCenter(vertices)
444 } else {
445 Point(centerX, centerY)
446 }
447 return RoundedPolygon(tempFeatures, cx, cy)
448 }
449
450 /**
451 * This constructor takes a list of [Feature] objects that define the polygon's shape and curves. By
452 * specifying the features directly, the summarization of [Cubic] objects to curves can be precisely
453 * controlled. This affects [Morph]'s default mapping, as curves with the same type (convex or
454 * concave) are mapped with each other. For example, if you have a convex curve in your start
455 * polygon, [Morph] will map it to another convex curve in the end polygon.
456 *
457 * The [centerX] and [centerY] parameters are optional. If not supplied, they will be estimated by
458 * calculating the average of all cubic anchor points.
459 *
460 * @param features The [Feature]s that describe the characteristics of each outline segment of the
461 * polygon.
462 * @param centerX The X coordinate of the center of the polygon, around which all vertices will be
463 * placed. If none provided, the center will be averaged.
464 * @param centerY The Y coordinate of the center of the polygon, around which all vertices will be
465 * placed. If none provided, the center will be averaged.
466 * @throws IllegalArgumentException [features] must be at least specify 2 features and describe a
467 * closed shape.
468 */
469 @JvmOverloads
RoundedPolygonnull470 fun RoundedPolygon(
471 features: List<Feature>,
472 centerX: Float = Float.NaN,
473 centerY: Float = Float.NaN
474 ): RoundedPolygon {
475 require(features.size >= 2) { "Polygons must have at least 2 features" }
476
477 val vertices =
478 buildList {
479 for (feature in features) {
480 for (cubic in feature.cubics) {
481 add(cubic.anchor0X)
482 add(cubic.anchor0Y)
483 }
484 }
485 }
486 .toFloatArray()
487
488 val cX = if (centerX.isNaN()) calculateCenter(vertices).first else centerX
489 val cY = if (centerY.isNaN()) calculateCenter(vertices).second else centerY
490
491 return RoundedPolygon(features, Point(cX, cY))
492 }
493
494 /**
495 * Calculates an estimated center position for the polygon, returning it. This function should only
496 * be called if the center is not already calculated or provided. The Polygon constructor which
497 * takes `numVertices` calculates its own center, since it knows exactly where it is centered, at
498 * (0, 0).
499 *
500 * Note that this center will be transformed whenever the shape itself is transformed. Any
501 * transforms that occur before the center is calculated will be taken into account automatically
502 * since the center calculation is an average of the current location of all cubic anchor points.
503 */
calculateCenternull504 internal fun calculateCenter(vertices: FloatArray): Point {
505 var cumulativeX = 0f
506 var cumulativeY = 0f
507 var index = 0
508 while (index < vertices.size) {
509 cumulativeX += vertices[index++]
510 cumulativeY += vertices[index++]
511 }
512 return Point(cumulativeX / (vertices.size / 2), cumulativeY / (vertices.size / 2))
513 }
514
515 /**
516 * Private utility class that holds the information about each corner in a polygon. The shape of the
517 * corner can be returned by calling the [getCubics] function, which will return a list of curves
518 * representing the corner geometry. The shape of the corner depends on the [rounding] constructor
519 * parameter.
520 *
521 * If rounding is null, there is no rounding; the corner will simply be a single point at [p1]. This
522 * point will be represented by a [Cubic] of length 0 at that point.
523 *
524 * If rounding is not null, the corner will be rounded either with a curve approximating a circular
525 * arc of the radius specified in [rounding], or with three curves if [rounding] has a nonzero
526 * smoothing parameter. These three curves are a circular arc in the middle and two symmetrical
527 * flanking curves on either side. The smoothing parameter determines the curvature of the flanking
528 * curves.
529 *
530 * This is a class because we usually need to do the work in 2 steps, and prefer to keep state
531 * between: first we determine how much we want to cut to comply with the parameters, then we are
532 * given how much we can actually cut (because of space restrictions outside this corner)
533 *
534 * @param p0 the vertex before the one being rounded
535 * @param p1 the vertex of this rounded corner
536 * @param p2 the vertex after the one being rounded
537 * @param rounding the optional parameters specifying how this corner should be rounded
538 */
539 private class RoundedCorner(
540 val p0: Point,
541 val p1: Point,
542 val p2: Point,
543 val rounding: CornerRounding? = null
544 ) {
545 val d1: Point
546 val d2: Point
547 val cornerRadius: Float
548 val smoothing: Float
549 val cosAngle: Float
550 val sinAngle: Float
551 val expectedRoundCut: Float
552
553 init {
554 val v01 = p0 - p1
555 val v21 = p2 - p1
556 val d01 = v01.getDistance()
557 val d21 = v21.getDistance()
558 if (d01 > 0f && d21 > 0f) {
559 d1 = v01 / d01
560 d2 = v21 / d21
561 cornerRadius = rounding?.radius ?: 0f
562 smoothing = rounding?.smoothing ?: 0f
563
564 // cosine of angle at p1 is dot product of unit vectors to the other two vertices
565 cosAngle = d1.dotProduct(d2)
566
567 // identity: sin^2 + cos^2 = 1
568 // sinAngle gives us the intersection
569 sinAngle = sqrt(1 - square(cosAngle))
570 // How much we need to cut, as measured on a side, to get the required radius
571 // calculating where the rounding circle hits the edge
572 // This uses the identity of tan(A/2) = sinA/(1 + cosA), where tan(A/2) = radius/cut
573 expectedRoundCut =
574 if (sinAngle > 1e-3) {
575 cornerRadius * (cosAngle + 1) / sinAngle
576 } else {
577 0f
578 }
579 } else {
580 // One (or both) of the sides is empty, not much we can do.
581 d1 = Point(0f, 0f)
582 d2 = Point(0f, 0f)
583 cornerRadius = 0f
584 smoothing = 0f
585 cosAngle = 0f
586 sinAngle = 0f
587 expectedRoundCut = 0f
588 }
589 }
590
591 // smoothing changes the actual cut. 0 is same as expectedRoundCut, 1 doubles it
592 val expectedCut: Float
593 get() = ((1 + smoothing) * expectedRoundCut)
594
595 // the center of the circle approximated by the rounding curve (or the middle of the three
596 // curves if smoothing is requested). The center is the same as p0 if there is no rounding.
597 var center: Point = Point(0f, 0f)
598
599 @JvmOverloads
getCubicsnull600 fun getCubics(allowedCut0: Float, allowedCut1: Float = allowedCut0): List<Cubic> {
601 // We use the minimum of both cuts to determine the radius, but if there is more space
602 // in one side we can use it for smoothing.
603 val allowedCut = min(allowedCut0, allowedCut1)
604 // Nothing to do, just use lines, or a point
605 if (
606 expectedRoundCut < DistanceEpsilon ||
607 allowedCut < DistanceEpsilon ||
608 cornerRadius < DistanceEpsilon
609 ) {
610 center = p1
611 return listOf(Cubic.straightLine(p1.x, p1.y, p1.x, p1.y))
612 }
613 // How much of the cut is required for the rounding part.
614 val actualRoundCut = min(allowedCut, expectedRoundCut)
615 // We have two smoothing values, one for each side of the vertex
616 // Space is used for rounding values first. If there is space left over, then we
617 // apply smoothing, if it was requested
618 val actualSmoothing0 = calculateActualSmoothingValue(allowedCut0)
619 val actualSmoothing1 = calculateActualSmoothingValue(allowedCut1)
620 // Scale the radius if needed
621 val actualR = cornerRadius * actualRoundCut / expectedRoundCut
622 // Distance from the corner (p1) to the center
623 val centerDistance = sqrt(square(actualR) + square(actualRoundCut))
624 // Center of the arc we will use for rounding
625 center = p1 + ((d1 + d2) / 2f).getDirection() * centerDistance
626 val circleIntersection0 = p1 + d1 * actualRoundCut
627 val circleIntersection2 = p1 + d2 * actualRoundCut
628 val flanking0 =
629 computeFlankingCurve(
630 actualRoundCut,
631 actualSmoothing0,
632 p1,
633 p0,
634 circleIntersection0,
635 circleIntersection2,
636 center,
637 actualR
638 )
639 val flanking2 =
640 computeFlankingCurve(
641 actualRoundCut,
642 actualSmoothing1,
643 p1,
644 p2,
645 circleIntersection2,
646 circleIntersection0,
647 center,
648 actualR
649 )
650 .reverse()
651 return listOf(
652 flanking0,
653 Cubic.circularArc(
654 center.x,
655 center.y,
656 flanking0.anchor1X,
657 flanking0.anchor1Y,
658 flanking2.anchor0X,
659 flanking2.anchor0Y
660 ),
661 flanking2
662 )
663 }
664
665 /**
666 * If allowedCut (the amount we are able to cut) is greater than the expected cut (without
667 * smoothing applied yet), then there is room to apply smoothing and we calculate the actual
668 * smoothing value here.
669 */
calculateActualSmoothingValuenull670 private fun calculateActualSmoothingValue(allowedCut: Float): Float {
671 return if (allowedCut > expectedCut) {
672 smoothing
673 } else if (allowedCut > expectedRoundCut) {
674 smoothing * (allowedCut - expectedRoundCut) / (expectedCut - expectedRoundCut)
675 } else {
676 0f
677 }
678 }
679
680 /**
681 * Compute a Bezier to connect the linear segment defined by corner and sideStart with the
682 * circular segment defined by circleCenter, circleSegmentIntersection,
683 * otherCircleSegmentIntersection and actualR. The bezier will start at the linear segment and
684 * end on the circular segment.
685 *
686 * @param actualRoundCut How much we are cutting of the corner to add the circular segment (this
687 * is before smoothing, that will cut some more).
688 * @param actualSmoothingValues How much we want to smooth (this is the smooth parameter,
689 * adjusted down if there is not enough room).
690 * @param corner The point at which the linear side ends
691 * @param sideStart The point at which the linear side starts
692 * @param circleSegmentIntersection The point at which the linear side and the circle intersect.
693 * @param otherCircleSegmentIntersection The point at which the opposing linear side and the
694 * circle intersect.
695 * @param circleCenter The center of the circle.
696 * @param actualR The radius of the circle.
697 * @return a Bezier cubic curve that connects from the (cut) linear side and the (cut) circular
698 * segment in a smooth way.
699 */
computeFlankingCurvenull700 private fun computeFlankingCurve(
701 actualRoundCut: Float,
702 actualSmoothingValues: Float,
703 corner: Point,
704 sideStart: Point,
705 circleSegmentIntersection: Point,
706 otherCircleSegmentIntersection: Point,
707 circleCenter: Point,
708 actualR: Float
709 ): Cubic {
710 // sideStart is the anchor, 'anchor' is actual control point
711 val sideDirection = (sideStart - corner).getDirection()
712 val curveStart = corner + sideDirection * actualRoundCut * (1 + actualSmoothingValues)
713 // We use an approximation to cut a part of the circle section proportional to 1 - smooth,
714 // When smooth = 0, we take the full section, when smooth = 1, we take nothing.
715 // TODO: revisit this, it can be problematic as it approaches 180 degrees
716 val p =
717 interpolate(
718 circleSegmentIntersection,
719 (circleSegmentIntersection + otherCircleSegmentIntersection) / 2f,
720 actualSmoothingValues
721 )
722 // The flanking curve ends on the circle
723 val curveEnd =
724 circleCenter + directionVector(p.x - circleCenter.x, p.y - circleCenter.y) * actualR
725 // The anchor on the circle segment side is in the intersection between the tangent to the
726 // circle in the circle/flanking curve boundary and the linear segment.
727 val circleTangent = (curveEnd - circleCenter).rotate90()
728 val anchorEnd =
729 lineIntersection(sideStart, sideDirection, curveEnd, circleTangent)
730 ?: circleSegmentIntersection
731 // From what remains, we pick a point for the start anchor.
732 // 2/3 seems to come from design tools?
733 val anchorStart = (curveStart + anchorEnd * 2f) / 3f
734 return Cubic(curveStart, anchorStart, anchorEnd, curveEnd)
735 }
736
737 /**
738 * Returns the intersection point of the two lines d0->d1 and p0->p1, or null if the lines do
739 * not intersect
740 */
lineIntersectionnull741 private fun lineIntersection(p0: Point, d0: Point, p1: Point, d1: Point): Point? {
742 val rotatedD1 = d1.rotate90()
743 val den = d0.dotProduct(rotatedD1)
744 if (abs(den) < DistanceEpsilon) return null
745 val num = (p1 - p0).dotProduct(rotatedD1)
746 // Also check the relative value. This is equivalent to abs(den/num) < DistanceEpsilon,
747 // but avoid doing a division
748 if (abs(den) < DistanceEpsilon * abs(num)) return null
749 val k = num / den
750 return p0 + d0 * k
751 }
752 }
753
verticesFromNumVertsnull754 private fun verticesFromNumVerts(
755 numVertices: Int,
756 radius: Float,
757 centerX: Float,
758 centerY: Float
759 ): FloatArray {
760 val result = FloatArray(numVertices * 2)
761 var arrayIndex = 0
762 for (i in 0 until numVertices) {
763 val vertex =
764 radialToCartesian(radius, (FloatPi / numVertices * 2 * i)) + Point(centerX, centerY)
765 result[arrayIndex++] = vertex.x
766 result[arrayIndex++] = vertex.y
767 }
768 return result
769 }
770