1 /*
2 * 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.collection.FloatFloatPair
20 import kotlin.jvm.JvmStatic
21 import kotlin.math.abs
22 import kotlin.math.max
23 import kotlin.math.min
24 import kotlin.math.sqrt
25
26 /**
27 * This class holds the anchor and control point data for a single cubic Bézier curve, with anchor
28 * points ([anchor0X], [anchor0Y]) and ([anchor1X], [anchor1Y]) at either end and control points
29 * ([control0X], [control0Y]) and ([control1X], [control1Y]) determining the slope of the curve
30 * between the anchor points.
31 */
32 open class Cubic internal constructor(internal val points: FloatArray = FloatArray(8)) {
33 init {
<lambda>null34 require(points.size == 8) { "Points array size should be 8" }
35 }
36
37 /** The first anchor point x coordinate */
38 val anchor0X
39 get() = points[0]
40
41 /** The first anchor point y coordinate */
42 val anchor0Y
43 get() = points[1]
44
45 /** The first control point x coordinate */
46 val control0X
47 get() = points[2]
48
49 /** The first control point y coordinate */
50 val control0Y
51 get() = points[3]
52
53 /** The second control point x coordinate */
54 val control1X
55 get() = points[4]
56
57 /** The second control point y coordinate */
58 val control1Y
59 get() = points[5]
60
61 /** The second anchor point x coordinate */
62 val anchor1X
63 get() = points[6]
64
65 /** The second anchor point y coordinate */
66 val anchor1Y
67 get() = points[7]
68
69 internal constructor(
70 anchor0: Point,
71 control0: Point,
72 control1: Point,
73 anchor1: Point
74 ) : this(
75 floatArrayOf(
76 anchor0.x,
77 anchor0.y,
78 control0.x,
79 control0.y,
80 control1.x,
81 control1.y,
82 anchor1.x,
83 anchor1.y
84 )
85 )
86
87 /**
88 * Returns a point on the curve for parameter t, representing the proportional distance along
89 * the curve between its starting point at anchor0 and ending point at anchor1.
90 *
91 * @param t The distance along the curve between the anchor points, where 0 is at anchor0 and 1
92 * is at anchor1
93 */
pointOnCurvenull94 internal fun pointOnCurve(t: Float): Point {
95 val u = 1 - t
96 return Point(
97 anchor0X * (u * u * u) +
98 control0X * (3 * t * u * u) +
99 control1X * (3 * t * t * u) +
100 anchor1X * (t * t * t),
101 anchor0Y * (u * u * u) +
102 control0Y * (3 * t * u * u) +
103 control1Y * (3 * t * t * u) +
104 anchor1Y * (t * t * t)
105 )
106 }
107
zeroLengthnull108 internal fun zeroLength() =
109 abs(anchor0X - anchor1X) < DistanceEpsilon && abs(anchor0Y - anchor1Y) < DistanceEpsilon
110
111 internal fun convexTo(next: Cubic): Boolean {
112 val prevVertex = Point(anchor0X, anchor0Y)
113 val currVertex = Point(anchor1X, anchor1Y)
114 val nextVertex = Point(next.anchor1X, next.anchor1Y)
115 return convex(prevVertex, currVertex, nextVertex)
116 }
117
zeroIshnull118 private fun zeroIsh(value: Float) = abs(value) < DistanceEpsilon
119
120 /**
121 * This function returns the true bounds of this curve, filling [bounds] with the axis-aligned
122 * bounding box values for left, top, right, and bottom, in that order.
123 */
124 internal fun calculateBounds(bounds: FloatArray = FloatArray(4), approximate: Boolean = false) {
125
126 // A curve might be of zero-length, with both anchors co-lated.
127 // Just return the point itself.
128 if (zeroLength()) {
129 bounds[0] = anchor0X
130 bounds[1] = anchor0Y
131 bounds[2] = anchor0X
132 bounds[3] = anchor0Y
133 return
134 }
135
136 var minX = min(anchor0X, anchor1X)
137 var minY = min(anchor0Y, anchor1Y)
138 var maxX = max(anchor0X, anchor1X)
139 var maxY = max(anchor0Y, anchor1Y)
140
141 if (approximate) {
142 // Approximate bounds use the bounding box of all anchors and controls
143 bounds[0] = min(minX, min(control0X, control1X))
144 bounds[1] = min(minY, min(control0Y, control1Y))
145 bounds[2] = max(maxX, max(control0X, control1X))
146 bounds[3] = max(maxY, max(control0Y, control1Y))
147 return
148 }
149
150 // Find the derivative, which is a quadratic Bezier. Then we can solve for t using
151 // the quadratic formula
152 val xa = -anchor0X + 3 * control0X - 3 * control1X + anchor1X
153 val xb = 2 * anchor0X - 4 * control0X + 2 * control1X
154 val xc = -anchor0X + control0X
155
156 if (zeroIsh(xa)) {
157 // Try Muller's method instead; it can find a single root when a is 0
158 if (xb != 0f) {
159 val t = 2 * xc / (-2 * xb)
160 if (t in 0f..1f) {
161 pointOnCurve(t).x.let {
162 if (it < minX) minX = it
163 if (it > maxX) maxX = it
164 }
165 }
166 }
167 } else {
168 val xs = xb * xb - 4 * xa * xc
169 if (xs >= 0) {
170 val t1 = (-xb + sqrt(xs)) / (2 * xa)
171 if (t1 in 0f..1f) {
172 pointOnCurve(t1).x.let {
173 if (it < minX) minX = it
174 if (it > maxX) maxX = it
175 }
176 }
177
178 val t2 = (-xb - sqrt(xs)) / (2 * xa)
179 if (t2 in 0f..1f) {
180 pointOnCurve(t2).x.let {
181 if (it < minX) minX = it
182 if (it > maxX) maxX = it
183 }
184 }
185 }
186 }
187
188 // Repeat the above for y coordinate
189 val ya = -anchor0Y + 3 * control0Y - 3 * control1Y + anchor1Y
190 val yb = 2 * anchor0Y - 4 * control0Y + 2 * control1Y
191 val yc = -anchor0Y + control0Y
192
193 if (zeroIsh(ya)) {
194 if (yb != 0f) {
195 val t = 2 * yc / (-2 * yb)
196 if (t in 0f..1f) {
197 pointOnCurve(t).y.let {
198 if (it < minY) minY = it
199 if (it > maxY) maxY = it
200 }
201 }
202 }
203 } else {
204 val ys = yb * yb - 4 * ya * yc
205 if (ys >= 0) {
206 val t1 = (-yb + sqrt(ys)) / (2 * ya)
207 if (t1 in 0f..1f) {
208 pointOnCurve(t1).y.let {
209 if (it < minY) minY = it
210 if (it > maxY) maxY = it
211 }
212 }
213
214 val t2 = (-yb - sqrt(ys)) / (2 * ya)
215 if (t2 in 0f..1f) {
216 pointOnCurve(t2).y.let {
217 if (it < minY) minY = it
218 if (it > maxY) maxY = it
219 }
220 }
221 }
222 }
223 bounds[0] = minX
224 bounds[1] = minY
225 bounds[2] = maxX
226 bounds[3] = maxY
227 }
228
229 /**
230 * Returns two Cubics, created by splitting this curve at the given distance of [t] between the
231 * original starting and ending anchor points.
232 */
233 // TODO: cartesian optimization?
splitnull234 fun split(t: Float): Pair<Cubic, Cubic> {
235 val u = 1 - t
236 val pointOnCurve = pointOnCurve(t)
237 return Cubic(
238 anchor0X,
239 anchor0Y,
240 anchor0X * u + control0X * t,
241 anchor0Y * u + control0Y * t,
242 anchor0X * (u * u) + control0X * (2 * u * t) + control1X * (t * t),
243 anchor0Y * (u * u) + control0Y * (2 * u * t) + control1Y * (t * t),
244 pointOnCurve.x,
245 pointOnCurve.y
246 ) to
247 Cubic(
248 // TODO: should calculate once and share the result
249 pointOnCurve.x,
250 pointOnCurve.y,
251 control0X * (u * u) + control1X * (2 * u * t) + anchor1X * (t * t),
252 control0Y * (u * u) + control1Y * (2 * u * t) + anchor1Y * (t * t),
253 control1X * u + anchor1X * t,
254 control1Y * u + anchor1Y * t,
255 anchor1X,
256 anchor1Y
257 )
258 }
259
260 /** Utility function to reverse the control/anchor points for this curve. */
reversenull261 fun reverse() =
262 Cubic(anchor1X, anchor1Y, control1X, control1Y, control0X, control0Y, anchor0X, anchor0Y)
263
264 /** Operator overload to enable adding Cubic objects together, like "c0 + c1" */
265 operator fun plus(o: Cubic) = Cubic(FloatArray(8) { points[it] + o.points[it] })
266
267 /** Operator overload to enable multiplying Cubics by a scalar value x, like "c0 * x" */
<lambda>null268 operator fun times(x: Float) = Cubic(FloatArray(8) { points[it] * x })
269
270 /** Operator overload to enable multiplying Cubics by an Int scalar value x, like "c0 * x" */
timesnull271 operator fun times(x: Int) = times(x.toFloat())
272
273 /** Operator overload to enable dividing Cubics by a scalar value x, like "c0 / x" */
274 operator fun div(x: Float) = times(1f / x)
275
276 /** Operator overload to enable dividing Cubics by a scalar value x, like "c0 / x" */
277 operator fun div(x: Int) = div(x.toFloat())
278
279 override fun toString(): String {
280 return "anchor0: ($anchor0X, $anchor0Y) control0: ($control0X, $control0Y), " +
281 "control1: ($control1X, $control1Y), anchor1: ($anchor1X, $anchor1Y)"
282 }
283
equalsnull284 override fun equals(other: Any?): Boolean {
285 if (this === other) return true
286
287 if (other !is Cubic) {
288 return false
289 }
290
291 return points.contentEquals(other.points)
292 }
293
294 /**
295 * Transforms the points in this [Cubic] with the given [PointTransformer] and returns a new
296 * [Cubic]
297 *
298 * @param f The [PointTransformer] used to transform this [Cubic]
299 */
transformednull300 fun transformed(f: PointTransformer): Cubic {
301 val newCubic = MutableCubic()
302 points.copyInto(newCubic.points)
303 newCubic.transform(f)
304 return newCubic
305 }
306
hashCodenull307 override fun hashCode() = points.contentHashCode()
308
309 companion object {
310 /**
311 * Generates a bezier curve that is a straight line between the given anchor points. The
312 * control points lie 1/3 of the distance from their respective anchor points.
313 */
314 @JvmStatic
315 fun straightLine(x0: Float, y0: Float, x1: Float, y1: Float): Cubic {
316 return Cubic(
317 x0,
318 y0,
319 interpolate(x0, x1, 1f / 3f),
320 interpolate(y0, y1, 1f / 3f),
321 interpolate(x0, x1, 2f / 3f),
322 interpolate(y0, y1, 2f / 3f),
323 x1,
324 y1
325 )
326 }
327
328 // TODO: consider a more general function (maybe in addition to this) that allows
329 // caller to get a list of curves surpassing 180 degrees
330 /**
331 * Generates a bezier curve that approximates a circular arc, with p0 and p1 as the starting
332 * and ending anchor points. The curve generated is the smallest of the two possible arcs
333 * around the entire 360-degree circle. Arcs of greater than 180 degrees should use more
334 * than one arc together. Note that p0 and p1 should be equidistant from the center.
335 */
336 @JvmStatic
337 fun circularArc(
338 centerX: Float,
339 centerY: Float,
340 x0: Float,
341 y0: Float,
342 x1: Float,
343 y1: Float
344 ): Cubic {
345 val p0d = directionVector(x0 - centerX, y0 - centerY)
346 val p1d = directionVector(x1 - centerX, y1 - centerY)
347 val rotatedP0 = p0d.rotate90()
348 val rotatedP1 = p1d.rotate90()
349 val clockwise = rotatedP0.dotProduct(x1 - centerX, y1 - centerY) >= 0
350 val cosa = p0d.dotProduct(p1d)
351 if (cosa > 0.999f) /* p0 ~= p1 */ return straightLine(x0, y0, x1, y1)
352 val k =
353 distance(x0 - centerX, y0 - centerY) * 4f / 3f *
354 (sqrt(2 * (1 - cosa)) - sqrt(1 - cosa * cosa)) / (1 - cosa) *
355 if (clockwise) 1f else -1f
356 return Cubic(
357 x0,
358 y0,
359 x0 + rotatedP0.x * k,
360 y0 + rotatedP0.y * k,
361 x1 - rotatedP1.x * k,
362 y1 - rotatedP1.y * k,
363 x1,
364 y1
365 )
366 }
367
368 /** Generates an empty Cubic defined at (x0, y0) */
369 @JvmStatic
370 internal fun empty(x0: Float, y0: Float): Cubic = Cubic(x0, y0, x0, y0, x0, y0, x0, y0)
371 }
372 }
373
374 /**
375 * Create a Cubic that holds the anchor and control point data for a single Bézier curve, with
376 * anchor points ([anchor0X], [anchor0Y]) and ([anchor1X], [anchor1Y]) at either end and control
377 * points ([control0X], [control0Y]) and ([control1X], [control1Y]) determining the slope of the
378 * curve between the anchor points.
379 *
380 * The returned instance is immutable.
381 *
382 * @param anchor0X the first anchor point x coordinate
383 * @param anchor0Y the first anchor point y coordinate
384 * @param control0X the first control point x coordinate
385 * @param control0Y the first control point y coordinate
386 * @param control1X the second control point x coordinate
387 * @param control1Y the second control point y coordinate
388 * @param anchor1X the second anchor point x coordinate
389 * @param anchor1Y the second anchor point y coordinate
390 */
Cubicnull391 fun Cubic(
392 anchor0X: Float,
393 anchor0Y: Float,
394 control0X: Float,
395 control0Y: Float,
396 control1X: Float,
397 control1Y: Float,
398 anchor1X: Float,
399 anchor1Y: Float
400 ) =
401 Cubic(
402 floatArrayOf(
403 anchor0X,
404 anchor0Y,
405 control0X,
406 control0Y,
407 control1X,
408 control1Y,
409 anchor1X,
410 anchor1Y
411 )
412 )
413
414 /** This interface is used refer to Points that can be modified, as a scope to [PointTransformer] */
415 interface MutablePoint {
416 /** The x coordinate of the Point */
417 var x: Float
418
419 /** The y coordinate of the Point */
420 var y: Float
421 }
422
423 typealias TransformResult = FloatFloatPair
424
425 /** Interface for a function that can transform (rotate/scale/translate/etc.) points. */
interfacenull426 fun interface PointTransformer {
427 /**
428 * Transform the point given the x and y parameters, returning the transformed point as a
429 * [TransformResult]
430 */
431 fun transform(x: Float, y: Float): TransformResult
432 }
433
434 /**
435 * This is a Mutable version of [Cubic], used mostly for performance critical paths so we can avoid
436 * creating new [Cubic]s
437 *
438 * This is used in Morph.forEachCubic, reusing a [MutableCubic] instance to avoid creating new
439 * [Cubic]s.
440 */
441 class MutableCubic : Cubic() {
transformOnePointnull442 private fun transformOnePoint(f: PointTransformer, ix: Int) {
443 val result = f.transform(points[ix], points[ix + 1])
444 points[ix] = result.first
445 points[ix + 1] = result.second
446 }
447
transformnull448 fun transform(f: PointTransformer) {
449 transformOnePoint(f, 0)
450 transformOnePoint(f, 2)
451 transformOnePoint(f, 4)
452 transformOnePoint(f, 6)
453 }
454
interpolatenull455 fun interpolate(c1: Cubic, c2: Cubic, progress: Float) {
456 repeat(8) { points[it] = interpolate(c1.points[it], c2.points[it], progress) }
457 }
458 }
459