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