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