Square moves across the screen
diff --git a/issue-repro/src/main/java/com/airbnb/lottie/issues/IssueReproActivity.kt b/issue-repro/src/main/java/com/airbnb/lottie/issues/IssueReproActivity.kt
index a48d7ff..7fc7b8a 100755
--- a/issue-repro/src/main/java/com/airbnb/lottie/issues/IssueReproActivity.kt
+++ b/issue-repro/src/main/java/com/airbnb/lottie/issues/IssueReproActivity.kt
@@ -15,11 +15,13 @@
         super.onCreate(savedInstanceState)
         setContent {
             val compositionResult = rememberLottieComposition(LottieAnimationSpec.RawRes(R.raw.anim))
+            val state = LottieAnimationState(
+                isPlaying = true,
+                repeatCount = Integer.MAX_VALUE,
+            )
             ComposeLottieAnimation(
                 compositionResult,
-                LottieAnimationState(
-                    isPlaying = true,
-                ),
+                state,
             )
         }
     }
diff --git a/issue-repro/src/main/res/raw/anim.json b/issue-repro/src/main/res/raw/anim.json
index febb9af..072ccdb 100644
--- a/issue-repro/src/main/res/raw/anim.json
+++ b/issue-repro/src/main/res/raw/anim.json
@@ -1 +1 @@
-{"v":"5.7.4","fr":60,"ip":0,"op":63,"w":400,"h":400,"nm":"Test","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[200,200,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[280.614,280.614],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-6.693,4.307],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":63,"st":0,"bm":0}],"markers":[]}
\ No newline at end of file
+{"v":"5.7.4","fr":60,"ip":0,"op":61,"w":400,"h":400,"nm":"Test","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[200,200,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[150,150],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[-125,-125],"to":[41.667,41.667],"ti":[-41.667,-41.667]},{"t":60,"s":[125,125]}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":61,"st":0,"bm":0}],"markers":[]}
\ No newline at end of file
diff --git a/lottie-compose/src/main/java/com/airbnb/lottie/compose/renderer/AnimatableValues.kt b/lottie-compose/src/main/java/com/airbnb/lottie/compose/renderer/AnimatableValues.kt
deleted file mode 100644
index 8bb6b8b..0000000
--- a/lottie-compose/src/main/java/com/airbnb/lottie/compose/renderer/AnimatableValues.kt
+++ /dev/null
@@ -1,154 +0,0 @@
-package com.airbnb.lottie.compose.renderer
-
-import android.graphics.PointF
-import androidx.compose.runtime.*
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Matrix
-import androidx.compose.ui.graphics.graphicsLayer
-import com.airbnb.lottie.animation.keyframe.PathKeyframe
-import com.airbnb.lottie.model.animatable.AnimatableTransform
-import com.airbnb.lottie.model.layer.Layer
-import com.airbnb.lottie.value.Keyframe
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.setValue
-import com.airbnb.lottie.value.ScaleXY
-import kotlin.math.cos
-import kotlin.math.sin
-import kotlin.math.tan
-
-@Composable
-private fun <T> List<Keyframe<T>>.asState(
-    progress: Float,
-    defaultValue: T? = null,
-    calculator: (keyframe: Keyframe<T>, interpolatedProgress: Float) -> T,
-): T {
-    if (isEmpty()) {
-        return remember { defaultValue ?: error("You must specify a default value if the keyframes are empty.") }
-    }
-
-    var lastCalculatedProgress by remember { mutableStateOf(0f) }
-    var value by remember { mutableStateOf(defaultValue ?: calculator(first(), 0f)) }
-    if (lastCalculatedProgress == progress) return value
-    lastCalculatedProgress = progress
-
-    var keyframeIndex by remember { mutableStateOf(0) }
-    val newKeyframeIndex = findKeyframeIndex(progress, keyframeIndex)
-    val keyframe = get(keyframeIndex)
-    // The keyframe didn't change and it is static.
-    if (keyframeIndex == newKeyframeIndex && keyframe.isStatic) return value
-
-    val interpolatedProgress by keyframe.getInterpolatedProgressIntoKeyframe(progress)
-    value = calculator(keyframe, interpolatedProgress)
-    return value
-}
-
-@Composable
-fun List<Keyframe<Float>>.asFloatState(progress: Float, defaultValue: Float = 0f): Float {
-    return asState(progress, defaultValue) { keyframe, interpolatedProgress ->
-        lerp(keyframe.startValue ?: 0f, keyframe.endValue ?: 0f, interpolatedProgress)
-    }
-}
-
-@Composable
-fun List<Keyframe<Int>>.asIntState(progress: Float, defaultValue: Int = 0): Int {
-    return asState(progress, defaultValue) { keyframe, interpolatedProgress ->
-        lerp(keyframe.startValue ?: 0, keyframe.endValue ?: 0, interpolatedProgress)
-    }
-}
-
-@Composable
-fun List<Keyframe<ScaleXY>>.asScaleXYState(progress: Float, defaultValue: ScaleXY = ScaleXY()): ScaleXY {
-    val outPoint = remember { ScaleXY() }
-    return asState(progress, defaultValue) { keyframe, interpolatedProgress ->
-        outPoint.set(
-            lerp(keyframe.startValue?.scaleX ?: 0f, keyframe.endValue?.scaleX ?: 0f, interpolatedProgress),
-            lerp(keyframe.startValue?.scaleY ?: 0f, keyframe.endValue?.scaleY ?: 0f, interpolatedProgress),
-        )
-        outPoint
-    }
-}
-
-@Composable
-fun List<Keyframe<PointF>>.asPointFState(progress: Float, defaultValue: PointF = PointF()): PointF {
-    val outPoint = remember { PointF() }
-    return asState(progress, defaultValue) { keyframe, interpolatedProgress ->
-        outPoint.set(
-            lerp(keyframe.startValue?.x ?: 0f, keyframe.endValue?.x ?: 0f, interpolatedProgress),
-            lerp(keyframe.startValue?.y ?: 0f, keyframe.endValue?.y ?: 0f, interpolatedProgress),
-        )
-        outPoint
-    }
-}
-
-@Composable
-fun List<PathKeyframe>.asPathToPointState(progress: Float, defaultValue: PointF = PointF()): State<PointF> {
-    if (isEmpty()) {
-        return remember { mutableStateOf(defaultValue) }
-    }
-
-    var lastCalculatedProgress by remember { mutableStateOf(0f) }
-    var value = remember { mutableStateOf(defaultValue ) }
-    if (lastCalculatedProgress == progress) return value
-    lastCalculatedProgress = progress
-
-    var keyframeIndex by remember { mutableStateOf(0) }
-    val newKeyframeIndex = findKeyframeIndex(progress, keyframeIndex)
-    val keyframe = get(keyframeIndex)
-    // The keyframe didn't change and it is static.
-    if (keyframeIndex == newKeyframeIndex && keyframe.isStatic) return value
-
-    val interpolatedProgress by keyframe.getInterpolatedProgressIntoKeyframe(progress)
-    val path = keyframe.path
-    if (path == null) {
-        value.value = keyframe.startValue ?: value.value
-    } else {
-        value.value = pointAlongPath(path, interpolatedProgress)
-    }
-    return value
-}
-
-@Composable
-fun pointAlongPath(path: android.graphics.Path, progress: Float): PointF {
-    val locs = remember { FloatArray(2) }
-    var value by remember { mutableStateOf(PointF()) }
-    val pathMeasure = remember { android.graphics.PathMeasure() }
-    LaunchedEffect(path) {
-        pathMeasure.setPath(path, false)
-    }
-    LaunchedEffect(path, progress) {
-        pathMeasure.getPosTan(progress * pathMeasure.length, locs, null)
-    }
-    value.set(locs[0], locs[1])
-    return value
-}
-
-private fun List<Keyframe<*>>.findKeyframeIndex(progress: Float, currentKeyframeIndex: Int): Int {
-    if (get(currentKeyframeIndex).containsProgress(progress)) return currentKeyframeIndex
-    // TODO: if speed is reversed, flip these two.
-    for (i in currentKeyframeIndex until size) {
-        if (get(i).containsProgress(progress)) return i
-    }
-    for (i in 0 until currentKeyframeIndex) {
-        if (get(0).containsProgress(progress)) return i
-    }
-    error("Unable to find keyframe for progress $progress.")
-}
-
-@Composable
-private fun Keyframe<*>.getInterpolatedProgressIntoKeyframe(progress: Float): State<Float> {
-    var interpolatedProgress = remember { mutableStateOf(0f) }
-    val linearProgress = lerp(startProgress, endProgress, progress)
-    interpolatedProgress.value = when (val i = interpolator) {
-        null -> linearProgress
-        else -> i.getInterpolation(linearProgress)
-    }
-    return interpolatedProgress
-}
-
-class LayerTransform(layer: Layer) {
-    private val transform: AnimatableTransform = layer.shapes.firstInstanceOf()
-
-    var progress by mutableStateOf(0f)
-
-    val position by derivedStateOf { transform.position?.keyframes?.asPointFState(progress) }
-}
\ No newline at end of file
diff --git a/lottie-compose/src/main/java/com/airbnb/lottie/compose/renderer/LayerTransform.kt b/lottie-compose/src/main/java/com/airbnb/lottie/compose/renderer/LayerTransform.kt
new file mode 100644
index 0000000..4974626
--- /dev/null
+++ b/lottie-compose/src/main/java/com/airbnb/lottie/compose/renderer/LayerTransform.kt
@@ -0,0 +1,72 @@
+package com.airbnb.lottie.compose.renderer
+
+import android.graphics.PointF
+import androidx.compose.runtime.*
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.composed
+import androidx.compose.ui.graphics.graphicsLayer
+import com.airbnb.lottie.model.animatable.AnimatableTransform
+import com.airbnb.lottie.model.layer.Layer
+import com.airbnb.lottie.value.Keyframe
+import kotlin.properties.Delegates
+
+fun Modifier.withTransform(transform: LayerTransform): Modifier {
+    if (transform.isIdentity) return this
+
+    // TODO: maybe use composed {}
+    return this.then(
+        Modifier.graphicsLayer(
+            translationX = transform.position.x,
+            translationY = transform.position.y,
+        )
+    )
+}
+
+class LayerTransform(private val transform: AnimatableTransform?) {
+    var progress by Delegates.observable(0f) { _, oldValue, newValue ->
+        if (oldValue == newValue) return@observable
+        updatePosition(newValue)
+    }
+
+    private var positionProgress = 0f
+    private var positionKeyframeIndex = 0
+    private var _position = mutableStateOf(POINT_F)
+    val position: PointF by _position
+
+    val isIdentity get() = transform == null || position.equals(0f, 0f)
+
+    private fun updatePosition(progress: Float) {
+        if (positionProgress == progress) return
+        val keyframes = transform?.position?.keyframes ?: return
+
+        val keyframe = keyframes.findKeyframeIndex(progress, positionKeyframeIndex)
+        val linearProgress = lerp(keyframe.startProgress, keyframe.endProgress, progress)
+        val interpolatedProgress = when (val i = keyframe.interpolator) {
+            null -> linearProgress
+            else -> i.getInterpolation(linearProgress)
+        }
+        _position.value.set(
+            lerp(keyframe.startValue?.x ?: 0f, keyframe.endValue?.x ?: 0f, interpolatedProgress),
+            lerp(keyframe.startValue?.y ?: 0f, keyframe.endValue?.y ?: 0f, interpolatedProgress),
+        )
+    }
+
+    companion object {
+        private val POINT_F = PointF()
+    }
+}
+
+private fun <T> List<Keyframe<T>>.findKeyframeIndex(progress: Float, currentKeyframeIndex: Int): Keyframe<T> {
+    var keyframe = get(currentKeyframeIndex)
+    if (keyframe.containsProgress(progress)) return keyframe
+    // TODO: if speed is reversed, flip these two.
+    for (i in currentKeyframeIndex until size) {
+        keyframe = get(i)
+        if (keyframe.containsProgress(progress)) return keyframe
+    }
+    for (i in 0 until currentKeyframeIndex) {
+        keyframe = get(i)
+        if (keyframe.containsProgress(progress)) return keyframe
+    }
+    return keyframe
+}
\ No newline at end of file
diff --git a/lottie-compose/src/main/java/com/airbnb/lottie/compose/renderer/LottieComposeRenderer.kt b/lottie-compose/src/main/java/com/airbnb/lottie/compose/renderer/LottieComposeRenderer.kt
index ae26900..20cb2c0 100644
--- a/lottie-compose/src/main/java/com/airbnb/lottie/compose/renderer/LottieComposeRenderer.kt
+++ b/lottie-compose/src/main/java/com/airbnb/lottie/compose/renderer/LottieComposeRenderer.kt
@@ -1,12 +1,22 @@
 package com.airbnb.lottie.compose.renderer
 
-import androidx.compose.foundation.Canvas
+import android.graphics.PointF
+import android.util.Log
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.size
 import androidx.compose.runtime.*
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.SolidColor
+import androidx.compose.ui.graphics.vector.*
+import androidx.compose.ui.unit.dp
+import com.airbnb.lottie.LottieComposition
 import com.airbnb.lottie.compose.LottieAnimationState
 import com.airbnb.lottie.compose.LottieCompositionResult
 import com.airbnb.lottie.model.animatable.AnimatableTransform
+import com.airbnb.lottie.model.content.RectangleShape
+import com.airbnb.lottie.model.content.ShapeFill
+import com.airbnb.lottie.model.content.ShapeGroup
 import com.airbnb.lottie.model.layer.Layer
 import java.util.concurrent.TimeUnit
 import kotlin.math.floor
@@ -49,7 +59,12 @@
     ) {
         composition.layers.forEach { layer ->
             when (layer.layerType) {
-                Layer.LayerType.SHAPE -> ShapeLayer(layer)
+                Layer.LayerType.SHAPE -> Image(
+                    shapeLayerPainter(composition, layer),
+                    contentDescription = null,
+                    // TODO: use a different size
+                    modifier = Modifier.size(256.dp)
+                )
                 else -> Unit
             }
         }
@@ -57,15 +72,98 @@
 }
 
 @Composable
-fun ShapeLayer(layer: Layer) {
+fun shapeLayerPainter(composition: LottieComposition, layer: Layer): VectorPainter {
     val progress = LocalLottieProgress.current
-    val animatableTransform: AnimatableTransform = remember(layer) { layer.shapes.firstInstanceOf() }
-    val transform by transformMatrix(animatableTransform)
-
-    Canvas(
-        modifier = Modifier
-            .withTransform(progress, animatableTransform)
-    ) {
-
+    val animatableTransform = remember(layer) { layer.shapes.firstOrNull { it is AnimatableTransform } as? AnimatableTransform }
+    val transform = LayerTransform(animatableTransform)
+    LaunchedEffect(progress) {
+        transform.progress = progress
     }
+
+    return rememberVectorPainter(
+        defaultWidth = 256.dp,
+        defaultHeight = 256.dp,
+    ) { viewportWidth, viewportHeight ->
+        Group(
+            name = layer.layerName,
+            translationX = transform.position.x,
+            translationY = transform.position.y,
+            scaleX = viewportWidth / composition.bounds.width(),
+            scaleY = viewportHeight / composition.bounds.height(),
+        ) {
+            PathData {
+                layer.shapes.forEach { shapeModel ->
+                    when (shapeModel) {
+                        is ShapeGroup -> ComposeShapeGroup(shapeModel)
+                        else -> Log.d("Gabe", "Don't know how to draw ${shapeModel::class.simpleName}")
+                    }
+                }
+            }
+        }
+    }
+}
+
+@Composable
+fun ComposeShapeGroup(shapeGroup: ShapeGroup) {
+    if (shapeGroup.isHidden || shapeGroup.items.isEmpty()) return
+    val transform = remember(shapeGroup) { LayerTransform(shapeGroup.items.lastOrNull() as? AnimatableTransform) }
+    val pathData = remember { mutableListOf<PathNode>() }
+    val progress = LocalLottieProgress.current
+    LaunchedEffect(progress) {
+        transform.progress = progress
+    }
+    pathData.clear()
+    Group(
+        name = shapeGroup.name,
+        translationX = transform.position.x,
+        translationY = transform.position.y,
+    ) {
+        for (model in shapeGroup.items) {
+            when (model) {
+                is RectangleShape -> {
+                    pathData += rectanglePathData(model)
+                }
+                is ShapeFill -> {
+                    ComposeShapeFill(model, pathData)
+                }
+            }
+            Log.d("Gabe", "Drawing ${model::class.simpleName} $model")
+        }
+    }
+}
+
+@Composable
+fun rectanglePathData(shape: RectangleShape): List<PathNode> {
+    val progress = LocalLottieProgress.current
+    // TODO: optimize this
+    val size = remember { PointF() }
+    val sizeKeyframe = shape.size.keyframes.firstOrNull { it.containsProgress(progress) } ?: return emptyList()
+    val linearSizeProgress = lerp(sizeKeyframe.startProgress, sizeKeyframe.endProgress, progress)
+    val interpolatedProgress = when (val i = sizeKeyframe.interpolator) {
+        null -> linearSizeProgress
+        else -> i.getInterpolation(linearSizeProgress)
+    }
+    size.set(
+        lerp(sizeKeyframe.startValue?.x ?: 0f, sizeKeyframe.endValue?.x ?: 0f, interpolatedProgress),
+        lerp(sizeKeyframe.startValue?.y ?: 0f, sizeKeyframe.endValue?.y ?: 0f, interpolatedProgress),
+    )
+
+    return PathData {
+        moveTo(0f, 0f)
+        lineTo(size.x, 0f)
+        lineTo(size.x, size.y)
+        lineTo(0f, size.y)
+        lineTo(0f, 0f)
+        close()
+    }
+}
+
+@Composable
+fun ComposeShapeFill(fill: ShapeFill, pathData: List<PathNode>) {
+    Log.d("Gabe", "Drawing fill with ${pathData.size} nodes")
+    Path(
+        pathData,
+        // TODO: use the real color
+        fill = SolidColor(Color.Red),
+    )
 }
\ No newline at end of file
diff --git a/lottie/src/main/java/com/airbnb/lottie/model/layer/Layer.java b/lottie/src/main/java/com/airbnb/lottie/model/layer/Layer.java
index 174de65..7bab3ab 100644
--- a/lottie/src/main/java/com/airbnb/lottie/model/layer/Layer.java
+++ b/lottie/src/main/java/com/airbnb/lottie/model/layer/Layer.java
@@ -133,6 +133,10 @@
     return layerType;
   }
 
+  public String getLayerName() {
+    return layerName;
+  }
+
   MatteType getMatteType() {
     return matteType;
   }