1 /*
<lambda>null2  * Copyright 2019 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.compose.animation.core
18 
19 import androidx.compose.runtime.Immutable
20 import androidx.compose.runtime.Stable
21 import androidx.compose.ui.graphics.computeCubicVerticalBounds
22 import androidx.compose.ui.graphics.evaluateCubic
23 import androidx.compose.ui.graphics.findFirstCubicRoot
24 import androidx.compose.ui.util.fastCoerceIn
25 import kotlin.math.max
26 
27 /**
28  * Easing is a way to adjust an animation’s fraction. Easing allows transitioning elements to speed
29  * up and slow down, rather than moving at a constant rate.
30  *
31  * Fraction is a value between 0 and 1.0 indicating our current point in the animation where 0
32  * represents the start and 1.0 represents the end.
33  *
34  * An [Easing] must map fraction=0.0 to 0.0 and fraction=1.0 to 1.0.
35  */
36 @Stable
37 public fun interface Easing {
38     public fun transform(fraction: Float): Float
39 }
40 
41 /**
42  * Elements that begin and end at rest use this standard easing. They speed up quickly and slow down
43  * gradually, in order to emphasize the end of the transition.
44  *
45  * Standard easing puts subtle attention at the end of an animation, by giving more time to
46  * deceleration than acceleration. It is the most common form of easing.
47  *
48  * This is equivalent to the Android `FastOutSlowInInterpolator`
49  */
50 public val FastOutSlowInEasing: Easing = CubicBezierEasing(0.4f, 0.0f, 0.2f, 1.0f)
51 
52 /**
53  * Incoming elements are animated using deceleration easing, which starts a transition at peak
54  * velocity (the fastest point of an element’s movement) and ends at rest.
55  *
56  * This is equivalent to the Android `LinearOutSlowInInterpolator`
57  */
58 public val LinearOutSlowInEasing: Easing = CubicBezierEasing(0.0f, 0.0f, 0.2f, 1.0f)
59 
60 /**
61  * Elements exiting a screen use acceleration easing, where they start at rest and end at peak
62  * velocity.
63  *
64  * This is equivalent to the Android `FastOutLinearInInterpolator`
65  */
66 public val FastOutLinearInEasing: Easing = CubicBezierEasing(0.4f, 0.0f, 1.0f, 1.0f)
67 
68 /**
69  * It returns fraction unmodified. This is useful as a default value for cases where a [Easing] is
70  * required but no actual easing is desired.
71  */
fractionnull72 public val LinearEasing: Easing = Easing { fraction -> fraction }
73 
74 // This is equal to 1f.ulp or 1f.nextUp() - 1f, but neither ulp nor nextUp() are part of all KMP
75 // targets, only JVM and native
76 private const val OneUlpAt1 = 1.1920929e-7f
77 
78 /**
79  * A cubic polynomial easing.
80  *
81  * The [CubicBezierEasing] class implements third-order Bézier curves.
82  *
83  * This is equivalent to the Android `PathInterpolator` when a single cubic Bézier curve is
84  * specified.
85  *
86  * Note: [CubicBezierEasing] instances are stateless and can be used concurrently from multiple
87  * threads.
88  *
89  * Rather than creating a new instance, consider using one of the common cubic [Easing]s:
90  *
91  * @param a The x coordinate of the first control point. The line through the point (0, 0) and the
92  *   first control point is tangent to the easing at the point (0, 0).
93  * @param b The y coordinate of the first control point. The line through the point (0, 0) and the
94  *   first control point is tangent to the easing at the point (0, 0).
95  * @param c The x coordinate of the second control point. The line through the point (1, 1) and the
96  *   second control point is tangent to the easing at the point (1, 1).
97  * @param d The y coordinate of the second control point. The line through the point (1, 1) and the
98  *   second control point is tangent to the easing at the point (1, 1).
99  * @see FastOutSlowInEasing
100  * @see LinearOutSlowInEasing
101  * @see FastOutLinearInEasing
102  */
103 @Immutable
104 public class CubicBezierEasing(
105     private val a: Float,
106     private val b: Float,
107     private val c: Float,
108     private val d: Float
109 ) : Easing {
110     private val min: Float
111     private val max: Float
112 
113     init {
<lambda>null114         requirePrecondition(!a.isNaN() && !b.isNaN() && !c.isNaN() && !d.isNaN()) {
115             "Parameters to CubicBezierEasing cannot be NaN. Actual parameters are: $a, $b, $c, $d."
116         }
117         val roots = FloatArray(5)
118         val extrema = computeCubicVerticalBounds(0.0f, b, d, 1.0f, roots, 0)
119         min = extrema.first
120         max = extrema.second
121     }
122 
123     /**
124      * Transforms the specified [fraction] in the range 0..1 by this cubic Bézier curve. To solve
125      * the curve, [fraction] is used as the x coordinate along the curve, and the corresponding y
126      * coordinate on the curve is returned. If no solution exists, this method throws an
127      * [IllegalArgumentException].
128      *
129      * @throws IllegalArgumentException If the cubic Bézier curve cannot be solved
130      */
transformnull131     override fun transform(fraction: Float): Float {
132         return if (fraction > 0f && fraction < 1f) {
133             // We translate the coordinates by the fraction when calling findFirstCubicRoot,
134             // but we need to make sure the translation can be done at 1.0f so we take at
135             // least 1 ulp at 1.0f
136             val f = max(fraction, OneUlpAt1)
137             val t =
138                 findFirstCubicRoot(
139                     0.0f - f,
140                     a - f,
141                     c - f,
142                     1.0f - f,
143                 )
144 
145             // No root, the cubic curve has no solution
146             if (t.isNaN()) {
147                 throwNoSolution(fraction)
148             }
149 
150             // Don't clamp the values since the curve might be used to over- or under-shoot
151             // The test above that checks if fraction is in ]0..1[ will ensure we start and
152             // end at 0 and 1 respectively
153             evaluateCubic(b, d, t).fastCoerceIn(min, max)
154         } else {
155             fraction
156         }
157     }
158 
throwNoSolutionnull159     private fun throwNoSolution(fraction: Float) {
160         throw IllegalArgumentException(
161             "The cubic curve with parameters ($a, $b, $c, $d) has no solution at $fraction"
162         )
163     }
164 
equalsnull165     override fun equals(other: Any?): Boolean {
166         return other is CubicBezierEasing &&
167             a == other.a &&
168             b == other.b &&
169             c == other.c &&
170             d == other.d
171     }
172 
hashCodenull173     override fun hashCode(): Int {
174         return ((a.hashCode() * 31 + b.hashCode()) * 31 + c.hashCode()) * 31 + d.hashCode()
175     }
176 
toStringnull177     override fun toString(): String = "CubicBezierEasing(a=$a, b=$b, c=$c, d=$d)"
178 }
179