1 /*
<lambda>null2  * Copyright 2021 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.graphics.vector
18 
19 import androidx.collection.MutableScatterMap
20 import androidx.collection.mutableScatterMapOf
21 import androidx.compose.animation.animateColor
22 import androidx.compose.animation.core.Easing
23 import androidx.compose.animation.core.FiniteAnimationSpec
24 import androidx.compose.animation.core.KeyframesSpec
25 import androidx.compose.animation.core.LinearEasing
26 import androidx.compose.animation.core.RepeatMode
27 import androidx.compose.animation.core.Transition
28 import androidx.compose.animation.core.animateFloat
29 import androidx.compose.animation.core.keyframes
30 import androidx.compose.animation.core.repeatable
31 import androidx.compose.animation.core.tween
32 import androidx.compose.runtime.Composable
33 import androidx.compose.runtime.State
34 import androidx.compose.runtime.derivedStateOf
35 import androidx.compose.runtime.remember
36 import androidx.compose.ui.graphics.Color
37 import androidx.compose.ui.graphics.SolidColor
38 import androidx.compose.ui.graphics.vector.PathNode
39 import androidx.compose.ui.graphics.vector.VectorConfig
40 import androidx.compose.ui.graphics.vector.VectorProperty
41 import androidx.compose.ui.util.fastCoerceIn
42 import androidx.compose.ui.util.fastForEach
43 import androidx.compose.ui.util.fastMap
44 import androidx.compose.ui.util.fastMaxBy
45 import androidx.compose.ui.util.fastSumBy
46 import androidx.compose.ui.util.fastZip
47 import androidx.compose.ui.util.lerp
48 
49 internal const val RepeatCountInfinite = -1
50 
51 internal sealed class Animator {
52     abstract val totalDuration: Int
53 
54     @Composable
55     fun createVectorConfig(
56         transition: Transition<Boolean>,
57         overallDuration: Int
58     ): StateVectorConfig {
59         return remember { StateVectorConfig() }
60             .also { config -> Configure(transition, config, overallDuration) }
61     }
62 
63     @Composable
64     fun Configure(
65         transition: Transition<Boolean>,
66         config: StateVectorConfig,
67         overallDuration: Int
68     ) {
69         val propertyValuesMap =
70             remember(overallDuration) {
71                 mutableScatterMapOf<String, PropertyValues<*>>().also {
72                     collectPropertyValues(it, overallDuration, 0)
73                 }
74             }
75         propertyValuesMap.forEach { propertyName, values ->
76             values.timestamps.sortBy { it.timeMillis }
77             val state = values.createState(transition, propertyName, overallDuration)
78             @Suppress("UNCHECKED_CAST")
79             when (propertyName) {
80                 "rotation" -> config.rotationState = state as State<Float>
81                 "pivotX" -> config.pivotXState = state as State<Float>
82                 "pivotY" -> config.pivotYState = state as State<Float>
83                 "scaleX" -> config.scaleXState = state as State<Float>
84                 "scaleY" -> config.scaleYState = state as State<Float>
85                 "translateX" -> config.translateXState = state as State<Float>
86                 "translateY" -> config.translateYState = state as State<Float>
87                 "fillAlpha" -> config.fillAlphaState = state as State<Float>
88                 "strokeWidth" -> config.strokeWidthState = state as State<Float>
89                 "strokeAlpha" -> config.strokeAlphaState = state as State<Float>
90                 "trimPathStart" -> config.trimPathStartState = state as State<Float>
91                 "trimPathEnd" -> config.trimPathEndState = state as State<Float>
92                 "trimPathOffset" -> config.trimPathOffsetState = state as State<Float>
93                 "fillColor" -> config.fillColorState = state as State<Color>
94                 "strokeColor" -> config.strokeColorState = state as State<Color>
95                 "pathData" -> config.pathDataState = state as State<List<PathNode>>
96                 else -> throw IllegalStateException("Unknown propertyName: $propertyName")
97             }
98         }
99     }
100 
101     abstract fun collectPropertyValues(
102         propertyValuesMap: MutableScatterMap<String, PropertyValues<*>>,
103         overallDuration: Int,
104         parentDelay: Int
105     )
106 }
107 
108 internal class Timestamp<T>(
109     val timeMillis: Int,
110     val durationMillis: Int,
111     val repeatCount: Int,
112     val repeatMode: RepeatMode,
113     val holder: PropertyValuesHolder<T>
114 ) {
asAnimationSpecnull115     fun asAnimationSpec(): FiniteAnimationSpec<T> {
116         @Suppress("UNCHECKED_CAST")
117         val spec =
118             when (holder) {
119                 is PropertyValuesHolderFloat -> holder.asKeyframeSpec(durationMillis)
120                 is PropertyValuesHolderColor -> holder.asKeyframeSpec(durationMillis)
121                 else -> throw RuntimeException("Unexpected value type: $holder")
122             }
123                 as KeyframesSpec<T>
124         return if (repeatCount == 0) {
125             spec
126         } else {
127             repeatable(
128                 iterations =
129                     if (repeatCount == RepeatCountInfinite) {
130                         Int.MAX_VALUE
131                     } else {
132                         repeatCount + 1
133                     },
134                 animation = spec,
135                 repeatMode = repeatMode
136             )
137         }
138     }
139 }
140 
141 internal sealed class PropertyValues<T> {
142     val timestamps = mutableListOf<Timestamp<T>>()
143 
144     @Composable
createStatenull145     abstract fun createState(
146         transition: Transition<Boolean>,
147         propertyName: String,
148         overallDuration: Int
149     ): State<T>
150 
151     protected fun createAnimationSpec(
152         overallDuration: Int
153     ): @Composable Transition.Segment<Boolean>.() -> FiniteAnimationSpec<T> {
154         return {
155             val spec =
156                 combined(
157                     timestamps.fastMap { timestamp ->
158                         timestamp.timeMillis to timestamp.asAnimationSpec()
159                     }
160                 )
161             if (targetState) spec else spec.reversed(overallDuration)
162         }
163     }
164 }
165 
166 private class FloatPropertyValues : PropertyValues<Float>() {
167 
168     @Composable
createStatenull169     override fun createState(
170         transition: Transition<Boolean>,
171         propertyName: String,
172         overallDuration: Int
173     ): State<Float> {
174         return transition.animateFloat(
175             transitionSpec = createAnimationSpec(overallDuration),
176             label = propertyName,
177             targetValueByState = { atEnd ->
178                 if (atEnd) {
179                     (timestamps.last().holder as PropertyValuesHolderFloat)
180                         .animatorKeyframes
181                         .last()
182                         .value
183                 } else {
184                     (timestamps.first().holder as PropertyValuesHolderFloat)
185                         .animatorKeyframes
186                         .first()
187                         .value
188                 }
189             }
190         )
191     }
192 }
193 
194 private class ColorPropertyValues : PropertyValues<Color>() {
195 
196     @Composable
createStatenull197     override fun createState(
198         transition: Transition<Boolean>,
199         propertyName: String,
200         overallDuration: Int
201     ): State<Color> {
202         return transition.animateColor(
203             transitionSpec = createAnimationSpec(overallDuration),
204             label = propertyName,
205             targetValueByState = { atEnd ->
206                 if (atEnd) {
207                     (timestamps.last().holder as PropertyValuesHolderColor)
208                         .animatorKeyframes
209                         .last()
210                         .value
211                 } else {
212                     (timestamps.first().holder as PropertyValuesHolderColor)
213                         .animatorKeyframes
214                         .first()
215                         .value
216                 }
217             }
218         )
219     }
220 }
221 
222 private class PathPropertyValues : PropertyValues<List<PathNode>>() {
223 
224     @Composable
createStatenull225     override fun createState(
226         transition: Transition<Boolean>,
227         propertyName: String,
228         overallDuration: Int
229     ): State<List<PathNode>> {
230         val timeState =
231             transition.animateFloat(
232                 transitionSpec = {
233                     val spec = tween<Float>(durationMillis = overallDuration, easing = LinearEasing)
234                     if (targetState) spec else spec.reversed(overallDuration)
235                 },
236                 label = propertyName
237             ) { atEnd ->
238                 if (atEnd) overallDuration.toFloat() else 0f
239             }
240         @Suppress("UnrememberedMutableState") // b/279909531
241         return derivedStateOf { interpolate(timeState.value) }
242     }
243 
interpolatenull244     private fun interpolate(timeMillis: Float): List<PathNode> {
245         val timestamp = timestamps.lastOrNull { it.timeMillis <= timeMillis } ?: timestamps.first()
246         var fraction = (timeMillis - timestamp.timeMillis) / timestamp.durationMillis
247         if (timestamp.repeatCount != 0) {
248             var count = 0
249             while (fraction > 1f) {
250                 fraction -= 1f
251                 count++
252             }
253             if (timestamp.repeatMode == RepeatMode.Reverse && count % 2 != 0) {
254                 fraction = 1f - fraction
255             }
256         }
257         return (timestamp.holder as PropertyValuesHolderPath).interpolate(fraction)
258     }
259 }
260 
261 internal data class ObjectAnimator(
262     val duration: Int,
263     val startDelay: Int,
264     val repeatCount: Int,
265     val repeatMode: RepeatMode,
266     val holders: List<PropertyValuesHolder<*>>
267 ) : Animator() {
268 
269     override val totalDuration =
270         if (repeatCount == RepeatCountInfinite) {
271             Int.MAX_VALUE
272         } else {
273             startDelay + duration * (repeatCount + 1)
274         }
275 
collectPropertyValuesnull276     override fun collectPropertyValues(
277         propertyValuesMap: MutableScatterMap<String, PropertyValues<*>>,
278         overallDuration: Int,
279         parentDelay: Int
280     ) {
281         holders.fastForEach { holder ->
282             when (holder) {
283                 is PropertyValuesHolder2D -> {
284                     // TODO(b/178978971): Implement path animation
285                 }
286                 is PropertyValuesHolderFloat -> {
287                     val values =
288                         propertyValuesMap[holder.propertyName] as FloatPropertyValues?
289                             ?: FloatPropertyValues()
290                     values.timestamps.add(
291                         Timestamp(
292                             parentDelay + startDelay,
293                             duration,
294                             repeatCount,
295                             repeatMode,
296                             holder
297                         )
298                     )
299                     propertyValuesMap[holder.propertyName] = values
300                 }
301                 is PropertyValuesHolderColor -> {
302                     val values =
303                         propertyValuesMap[holder.propertyName] as ColorPropertyValues?
304                             ?: ColorPropertyValues()
305                     values.timestamps.add(
306                         Timestamp(
307                             parentDelay + startDelay,
308                             duration,
309                             repeatCount,
310                             repeatMode,
311                             holder
312                         )
313                     )
314                     propertyValuesMap[holder.propertyName] = values
315                 }
316                 is PropertyValuesHolderPath -> {
317                     val values =
318                         propertyValuesMap[holder.propertyName] as PathPropertyValues?
319                             ?: PathPropertyValues()
320                     values.timestamps.add(
321                         Timestamp(
322                             parentDelay + startDelay,
323                             duration,
324                             repeatCount,
325                             repeatMode,
326                             holder
327                         )
328                     )
329                     propertyValuesMap[holder.propertyName] = values
330                 }
331                 is PropertyValuesHolderInt -> {
332                     // Not implemented since AVD does not use any Int property.
333                 }
334             }
335         }
336     }
337 }
338 
339 internal data class AnimatorSet(val animators: List<Animator>, val ordering: Ordering) :
340     Animator() {
341 
342     override val totalDuration =
343         when (ordering) {
<lambda>null344             Ordering.Together -> animators.fastMaxBy { it.totalDuration }?.totalDuration ?: 0
<lambda>null345             Ordering.Sequentially -> animators.fastSumBy { it.totalDuration }
346         }
347 
collectPropertyValuesnull348     override fun collectPropertyValues(
349         propertyValuesMap: MutableScatterMap<String, PropertyValues<*>>,
350         overallDuration: Int,
351         parentDelay: Int
352     ) {
353         when (ordering) {
354             Ordering.Together -> {
355                 animators.fastForEach { animator ->
356                     animator.collectPropertyValues(propertyValuesMap, overallDuration, parentDelay)
357                 }
358             }
359             Ordering.Sequentially -> {
360                 var accumulatedDelay = parentDelay
361                 animators.fastForEach { animator ->
362                     animator.collectPropertyValues(
363                         propertyValuesMap,
364                         overallDuration,
365                         accumulatedDelay
366                     )
367                     accumulatedDelay += animator.totalDuration
368                 }
369             }
370         }
371     }
372 }
373 
374 internal sealed class PropertyValuesHolder<T>
375 
376 internal data class PropertyValuesHolder2D(
377     val xPropertyName: String,
378     val yPropertyName: String,
379     val pathData: List<PathNode>,
380     val interpolator: Easing
381 ) : PropertyValuesHolder<Pair<Float, Float>>()
382 
383 internal sealed class PropertyValuesHolder1D<T>(val propertyName: String) :
384     PropertyValuesHolder<T>() {
385 
386     abstract val animatorKeyframes: List<Keyframe<T>>
387 }
388 
389 internal class PropertyValuesHolderFloat(
390     propertyName: String,
391     override val animatorKeyframes: List<Keyframe<Float>>
392 ) : PropertyValuesHolder1D<Float>(propertyName) {
393 
asKeyframeSpecnull394     fun asKeyframeSpec(duration: Int): KeyframesSpec<Float> {
395         return keyframes {
396             durationMillis = duration
397             animatorKeyframes.fastForEach { keyframe ->
398                 keyframe.value at (duration * keyframe.fraction).toInt() using keyframe.interpolator
399             }
400         }
401     }
402 }
403 
404 internal class PropertyValuesHolderInt(
405     propertyName: String,
406     override val animatorKeyframes: List<Keyframe<Int>>
407 ) : PropertyValuesHolder1D<Int>(propertyName)
408 
409 internal class PropertyValuesHolderColor(
410     propertyName: String,
411     override val animatorKeyframes: List<Keyframe<Color>>
412 ) : PropertyValuesHolder1D<Color>(propertyName) {
413 
asKeyframeSpecnull414     fun asKeyframeSpec(duration: Int): KeyframesSpec<Color> {
415         return keyframes {
416             durationMillis = duration
417             animatorKeyframes.fastForEach { keyframe ->
418                 keyframe.value at (duration * keyframe.fraction).toInt() using keyframe.interpolator
419             }
420         }
421     }
422 }
423 
424 internal class PropertyValuesHolderPath(
425     propertyName: String,
426     override val animatorKeyframes: List<Keyframe<List<PathNode>>>
427 ) : PropertyValuesHolder1D<List<PathNode>>(propertyName) {
428 
interpolatenull429     fun interpolate(fraction: Float): List<PathNode> {
430         val index =
431             (animatorKeyframes.indexOfFirst { it.fraction >= fraction } - 1).coerceAtLeast(0)
432         val easing = animatorKeyframes[index + 1].interpolator
433         val innerFraction =
434             easing.transform(
435                 ((fraction - animatorKeyframes[index].fraction) /
436                         (animatorKeyframes[index + 1].fraction - animatorKeyframes[index].fraction))
437                     .fastCoerceIn(0f, 1f)
438             )
439         return lerp(
440             animatorKeyframes[index].value,
441             animatorKeyframes[index + 1].value,
442             innerFraction
443         )
444     }
445 }
446 
447 internal data class Keyframe<T>(val fraction: Float, val value: T, val interpolator: Easing)
448 
449 internal enum class Ordering {
450     Together,
451     Sequentially
452 }
453 
454 internal class StateVectorConfig : VectorConfig {
455 
456     var rotationState: State<Float>? = null
457     var pivotXState: State<Float>? = null
458     var pivotYState: State<Float>? = null
459     var scaleXState: State<Float>? = null
460     var scaleYState: State<Float>? = null
461     var translateXState: State<Float>? = null
462     var translateYState: State<Float>? = null
463     var pathDataState: State<List<PathNode>>? = null
464     var fillColorState: State<Color>? = null
465     var strokeColorState: State<Color>? = null
466     var strokeWidthState: State<Float>? = null
467     var strokeAlphaState: State<Float>? = null
468     var fillAlphaState: State<Float>? = null
469     var trimPathStartState: State<Float>? = null
470     var trimPathEndState: State<Float>? = null
471     var trimPathOffsetState: State<Float>? = null
472 
473     @Suppress("UNCHECKED_CAST")
getOrDefaultnull474     override fun <T> getOrDefault(property: VectorProperty<T>, defaultValue: T): T {
475         return when (property) {
476             is VectorProperty.Rotation -> rotationState?.value ?: defaultValue
477             is VectorProperty.PivotX -> pivotXState?.value ?: defaultValue
478             is VectorProperty.PivotY -> pivotYState?.value ?: defaultValue
479             is VectorProperty.ScaleX -> scaleXState?.value ?: defaultValue
480             is VectorProperty.ScaleY -> scaleYState?.value ?: defaultValue
481             is VectorProperty.TranslateX -> translateXState?.value ?: defaultValue
482             is VectorProperty.TranslateY -> translateYState?.value ?: defaultValue
483             is VectorProperty.PathData -> pathDataState?.value ?: defaultValue
484             is VectorProperty.Fill ->
485                 fillColorState?.let { state -> SolidColor(state.value) } ?: defaultValue
486             is VectorProperty.FillAlpha -> fillAlphaState?.value ?: defaultValue
487             is VectorProperty.Stroke ->
488                 strokeColorState?.let { state -> SolidColor(state.value) } ?: defaultValue
489             is VectorProperty.StrokeLineWidth -> strokeWidthState?.value ?: defaultValue
490             is VectorProperty.StrokeAlpha -> strokeAlphaState?.value ?: defaultValue
491             is VectorProperty.TrimPathStart -> trimPathStartState?.value ?: defaultValue
492             is VectorProperty.TrimPathEnd -> trimPathEndState?.value ?: defaultValue
493             is VectorProperty.TrimPathOffset -> trimPathOffsetState?.value ?: defaultValue
494         }
495             as T
496     }
497 
mergenull498     fun merge(config: StateVectorConfig) {
499         if (config.rotationState != null) rotationState = config.rotationState
500         if (config.pivotXState != null) pivotXState = config.pivotXState
501         if (config.pivotYState != null) pivotYState = config.pivotYState
502         if (config.scaleXState != null) scaleXState = config.scaleXState
503         if (config.scaleYState != null) scaleYState = config.scaleYState
504         if (config.translateXState != null) translateXState = config.translateXState
505         if (config.translateYState != null) translateYState = config.translateYState
506         if (config.pathDataState != null) pathDataState = config.pathDataState
507         if (config.fillColorState != null) fillColorState = config.fillColorState
508         if (config.strokeColorState != null) strokeColorState = config.strokeColorState
509         if (config.strokeWidthState != null) strokeWidthState = config.strokeWidthState
510         if (config.strokeAlphaState != null) strokeAlphaState = config.strokeAlphaState
511         if (config.fillAlphaState != null) fillAlphaState = config.fillAlphaState
512         if (config.trimPathStartState != null) trimPathStartState = config.trimPathStartState
513         if (config.trimPathEndState != null) trimPathEndState = config.trimPathEndState
514         if (config.trimPathOffsetState != null) trimPathOffsetState = config.trimPathOffsetState
515     }
516 }
517 
lerpnull518 private fun lerp(start: List<PathNode>, stop: List<PathNode>, fraction: Float): List<PathNode> {
519     return start.fastZip(stop) { a, b -> lerp(a, b, fraction) }
520 }
521 
522 private const val DifferentStartAndStopPathNodes = "start and stop path nodes have different types"
523 
524 /** Linearly interpolate between [start] and [stop] with [fraction] fraction between them. */
lerpnull525 private fun lerp(start: PathNode, stop: PathNode, fraction: Float): PathNode {
526     return when (start) {
527         is PathNode.RelativeMoveTo -> {
528             require(stop is PathNode.RelativeMoveTo) { DifferentStartAndStopPathNodes }
529             PathNode.RelativeMoveTo(
530                 lerp(start.dx, stop.dx, fraction),
531                 lerp(start.dy, stop.dy, fraction)
532             )
533         }
534         is PathNode.MoveTo -> {
535             require(stop is PathNode.MoveTo) { DifferentStartAndStopPathNodes }
536             PathNode.MoveTo(lerp(start.x, stop.x, fraction), lerp(start.y, stop.y, fraction))
537         }
538         is PathNode.RelativeLineTo -> {
539             require(stop is PathNode.RelativeLineTo) { DifferentStartAndStopPathNodes }
540             PathNode.RelativeLineTo(
541                 lerp(start.dx, stop.dx, fraction),
542                 lerp(start.dy, stop.dy, fraction)
543             )
544         }
545         is PathNode.LineTo -> {
546             require(stop is PathNode.LineTo) { DifferentStartAndStopPathNodes }
547             PathNode.LineTo(lerp(start.x, stop.x, fraction), lerp(start.y, stop.y, fraction))
548         }
549         is PathNode.RelativeHorizontalTo -> {
550             require(stop is PathNode.RelativeHorizontalTo) { DifferentStartAndStopPathNodes }
551             PathNode.RelativeHorizontalTo(lerp(start.dx, stop.dx, fraction))
552         }
553         is PathNode.HorizontalTo -> {
554             require(stop is PathNode.HorizontalTo) { DifferentStartAndStopPathNodes }
555             PathNode.HorizontalTo(lerp(start.x, stop.x, fraction))
556         }
557         is PathNode.RelativeVerticalTo -> {
558             require(stop is PathNode.RelativeVerticalTo) { DifferentStartAndStopPathNodes }
559             PathNode.RelativeVerticalTo(lerp(start.dy, stop.dy, fraction))
560         }
561         is PathNode.VerticalTo -> {
562             require(stop is PathNode.VerticalTo) { DifferentStartAndStopPathNodes }
563             PathNode.VerticalTo(lerp(start.y, stop.y, fraction))
564         }
565         is PathNode.RelativeCurveTo -> {
566             require(stop is PathNode.RelativeCurveTo) { DifferentStartAndStopPathNodes }
567             PathNode.RelativeCurveTo(
568                 lerp(start.dx1, stop.dx1, fraction),
569                 lerp(start.dy1, stop.dy1, fraction),
570                 lerp(start.dx2, stop.dx2, fraction),
571                 lerp(start.dy2, stop.dy2, fraction),
572                 lerp(start.dx3, stop.dx3, fraction),
573                 lerp(start.dy3, stop.dy3, fraction)
574             )
575         }
576         is PathNode.CurveTo -> {
577             require(stop is PathNode.CurveTo) { DifferentStartAndStopPathNodes }
578             PathNode.CurveTo(
579                 lerp(start.x1, stop.x1, fraction),
580                 lerp(start.y1, stop.y1, fraction),
581                 lerp(start.x2, stop.x2, fraction),
582                 lerp(start.y2, stop.y2, fraction),
583                 lerp(start.x3, stop.x3, fraction),
584                 lerp(start.y3, stop.y3, fraction)
585             )
586         }
587         is PathNode.RelativeReflectiveCurveTo -> {
588             require(stop is PathNode.RelativeReflectiveCurveTo) { DifferentStartAndStopPathNodes }
589             PathNode.RelativeReflectiveCurveTo(
590                 lerp(start.dx1, stop.dx1, fraction),
591                 lerp(start.dy1, stop.dy1, fraction),
592                 lerp(start.dx2, stop.dx2, fraction),
593                 lerp(start.dy2, stop.dy2, fraction)
594             )
595         }
596         is PathNode.ReflectiveCurveTo -> {
597             require(stop is PathNode.ReflectiveCurveTo) { DifferentStartAndStopPathNodes }
598             PathNode.ReflectiveCurveTo(
599                 lerp(start.x1, stop.x1, fraction),
600                 lerp(start.y1, stop.y1, fraction),
601                 lerp(start.x2, stop.x2, fraction),
602                 lerp(start.y2, stop.y2, fraction)
603             )
604         }
605         is PathNode.RelativeQuadTo -> {
606             require(stop is PathNode.RelativeQuadTo) { DifferentStartAndStopPathNodes }
607             PathNode.RelativeQuadTo(
608                 lerp(start.dx1, stop.dx1, fraction),
609                 lerp(start.dy1, stop.dy1, fraction),
610                 lerp(start.dx2, stop.dx2, fraction),
611                 lerp(start.dy2, stop.dy2, fraction)
612             )
613         }
614         is PathNode.QuadTo -> {
615             require(stop is PathNode.QuadTo) { DifferentStartAndStopPathNodes }
616             PathNode.QuadTo(
617                 lerp(start.x1, stop.x1, fraction),
618                 lerp(start.y1, stop.y1, fraction),
619                 lerp(start.x2, stop.x2, fraction),
620                 lerp(start.y2, stop.y2, fraction)
621             )
622         }
623         is PathNode.RelativeReflectiveQuadTo -> {
624             require(stop is PathNode.RelativeReflectiveQuadTo) { DifferentStartAndStopPathNodes }
625             PathNode.RelativeReflectiveQuadTo(
626                 lerp(start.dx, stop.dx, fraction),
627                 lerp(start.dy, stop.dy, fraction)
628             )
629         }
630         is PathNode.ReflectiveQuadTo -> {
631             require(stop is PathNode.ReflectiveQuadTo) { DifferentStartAndStopPathNodes }
632             PathNode.ReflectiveQuadTo(
633                 lerp(start.x, stop.x, fraction),
634                 lerp(start.y, stop.y, fraction)
635             )
636         }
637         is PathNode.RelativeArcTo -> {
638             require(stop is PathNode.RelativeArcTo) { DifferentStartAndStopPathNodes }
639             PathNode.RelativeArcTo(
640                 lerp(start.horizontalEllipseRadius, stop.horizontalEllipseRadius, fraction),
641                 lerp(start.verticalEllipseRadius, stop.verticalEllipseRadius, fraction),
642                 lerp(start.theta, stop.theta, fraction),
643                 start.isMoreThanHalf,
644                 start.isPositiveArc,
645                 lerp(start.arcStartDx, stop.arcStartDx, fraction),
646                 lerp(start.arcStartDy, stop.arcStartDy, fraction)
647             )
648         }
649         is PathNode.ArcTo -> {
650             require(stop is PathNode.ArcTo) { DifferentStartAndStopPathNodes }
651             PathNode.ArcTo(
652                 lerp(start.horizontalEllipseRadius, stop.horizontalEllipseRadius, fraction),
653                 lerp(start.verticalEllipseRadius, stop.verticalEllipseRadius, fraction),
654                 lerp(start.theta, stop.theta, fraction),
655                 start.isMoreThanHalf,
656                 start.isPositiveArc,
657                 lerp(start.arcStartX, stop.arcStartX, fraction),
658                 lerp(start.arcStartY, stop.arcStartY, fraction)
659             )
660         }
661         PathNode.Close -> PathNode.Close
662     }
663 }
664