Default loading and dynamic properties works
diff --git a/lottie-compose/src/main/java/com/airbnb/lottie/compose/LottieDynamicProperties.kt b/lottie-compose/src/main/java/com/airbnb/lottie/compose/LottieDynamicProperties.kt
index 653311e..ef791a9 100644
--- a/lottie-compose/src/main/java/com/airbnb/lottie/compose/LottieDynamicProperties.kt
+++ b/lottie-compose/src/main/java/com/airbnb/lottie/compose/LottieDynamicProperties.kt
@@ -2,6 +2,7 @@
import android.graphics.ColorFilter
import android.graphics.PointF
+import android.graphics.Typeface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
@@ -95,6 +96,7 @@
private val scaleProperties: List<LottieDynamicProperty<ScaleXY>>,
private val colorFilterProperties: List<LottieDynamicProperty<ColorFilter>>,
private val intArrayProperties: List<LottieDynamicProperty<IntArray>>,
+ private val typefaceProperties: List<LottieDynamicProperty<Typeface>>,
) {
@Suppress("UNCHECKED_CAST")
constructor(properties: List<LottieDynamicProperty<*>>) : this(
@@ -104,6 +106,7 @@
properties.filter { it.property is ScaleXY } as List<LottieDynamicProperty<ScaleXY>>,
properties.filter { it.property is ColorFilter } as List<LottieDynamicProperty<ColorFilter>>,
properties.filter { it.property is IntArray } as List<LottieDynamicProperty<IntArray>>,
+ properties.filter { it.property is Typeface } as List<LottieDynamicProperty<Typeface>>,
)
internal fun addTo(drawable: LottieDrawable) {
@@ -125,6 +128,9 @@
intArrayProperties.forEach { p ->
drawable.addValueCallback(p.keyPath, p.property, p.callback.toValueCallback())
}
+ typefaceProperties.forEach { p ->
+ drawable.addValueCallback(p.keyPath, p.property, p.callback.toValueCallback())
+ }
}
internal fun removeFrom(drawable: LottieDrawable) {
@@ -146,6 +152,9 @@
intArrayProperties.forEach { p ->
drawable.addValueCallback(p.keyPath, p.property, null as LottieValueCallback<IntArray>?)
}
+ typefaceProperties.forEach { p ->
+ drawable.addValueCallback(p.keyPath, p.property, null as LottieValueCallback<Typeface>?)
+ }
}
}
diff --git a/lottie-compose/src/main/java/com/airbnb/lottie/compose/rememberLottieComposition.kt b/lottie-compose/src/main/java/com/airbnb/lottie/compose/rememberLottieComposition.kt
index b9c0d51..e3ae50b 100644
--- a/lottie-compose/src/main/java/com/airbnb/lottie/compose/rememberLottieComposition.kt
+++ b/lottie-compose/src/main/java/com/airbnb/lottie/compose/rememberLottieComposition.kt
@@ -2,6 +2,7 @@
import android.content.Context
import android.graphics.BitmapFactory
+import android.graphics.Typeface
import android.util.Base64
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@@ -13,6 +14,7 @@
import com.airbnb.lottie.LottieCompositionFactory
import com.airbnb.lottie.LottieImageAsset
import com.airbnb.lottie.LottieTask
+import com.airbnb.lottie.model.Font
import com.airbnb.lottie.utils.Logger
import com.airbnb.lottie.utils.Utils
import kotlinx.coroutines.Dispatchers
@@ -45,6 +47,18 @@
* @param imageAssetsFolder A subfolder in `src/main/assets` that contains the exported images
* that this composition uses. DO NOT rename any images from your design tool. The
* filenames must match the values that are in your json file.
+ * @param fontAssetsFolder The default folder Lottie will look in to find font files. Fonts will be matched
+ * based on the family name specified in the Lottie json file.
+ * Defaults to "fonts/" so if "Helvetica" was in the Json file, Lottie will auto-match
+ * fonts located at "src/main/assets/fonts/Helvetica.ttf". Missing fonts will be skipped
+ * and should be set via fontRemapping or via dynamic properties.
+ * @param fontFileExtension The default file extension for font files specified in the fontAssetsFolder or fontRemapping.
+ * Defaults to ttf.
+ * @param fontRemapping Remaps family names as specified in the Lottie json file to font files stored in the fontAssetsFolder.
+ * This will automatically add the fontFileExtension so you should not include the font file extension
+ * in your remapping.
+ * @param cacheComposition Whether or not to cache the composition. If set to true, the next time an composition with this
+ * spec is fetched, it will return the existing one instead of parsing it again.
* @param onRetry An optional callback that will be called if loading the animation fails.
* It is passed the failed count (the number of times it has failed) and the exception
* from the previous attempt to load the composition. [onRetry] is a suspending function
@@ -55,6 +69,9 @@
fun rememberLottieComposition(
spec: LottieCompositionSpec,
imageAssetsFolder: String? = null,
+ fontAssetsFolder: String = "fonts/",
+ fontFileExtension: String = ".ttf",
+ fontRemapping: Map<String, String> = emptyMap(),
cacheComposition: Boolean = true,
onRetry: suspend (failCount: Int, previousException: Throwable) -> Boolean = { _, _ -> false },
): LottieCompositionResult {
@@ -69,6 +86,9 @@
context,
spec,
imageAssetsFolder.ensureTrailingSlash(),
+ fontAssetsFolder.ensureTrailingSlash(),
+ fontFileExtension.ensureLeadingPeriod(),
+ fontRemapping,
cacheComposition,
)
result.complete(composition)
@@ -88,6 +108,9 @@
context: Context,
spec: LottieCompositionSpec,
imageAssetsFolder: String?,
+ fontAssetsFolder: String?,
+ fontFileExtension: String,
+ fontRemapping: Map<String, String>,
cacheComposition: Boolean,
): LottieComposition {
val task = when (spec) {
@@ -111,7 +134,10 @@
FileInputStream(spec.fileName)
}
when {
- spec.fileName.endsWith("zip") -> LottieCompositionFactory.fromZipStream(ZipInputStream(fis), spec.fileName.takeIf { cacheComposition })
+ spec.fileName.endsWith("zip") -> LottieCompositionFactory.fromZipStream(
+ ZipInputStream(fis),
+ spec.fileName.takeIf { cacheComposition },
+ )
else -> LottieCompositionFactory.fromJsonInputStream(fis, spec.fileName.takeIf { cacheComposition })
}
}
@@ -129,6 +155,7 @@
val composition = task.await()
loadImagesFromAssets(context, composition, imageAssetsFolder)
+ loadFontsFromAssets(context, composition, fontAssetsFolder, fontFileExtension, fontRemapping)
return composition
}
@@ -198,8 +225,63 @@
}
}
+private suspend fun loadFontsFromAssets(
+ context: Context,
+ composition: LottieComposition,
+ fontAssetsFolder: String?,
+ fontFileExtension: String,
+ fontRemapping: Map<String, String>,
+) {
+ if (composition.fonts.isEmpty()) return
+ withContext(Dispatchers.IO) {
+ for (font in composition.fonts.values) {
+ maybeLoadTypefaceFromAssets(context, font, fontAssetsFolder, fontFileExtension, fontRemapping[font.family])
+ }
+ }
+}
+
+private fun maybeLoadTypefaceFromAssets(
+ context: Context,
+ font: Font,
+ fontAssetsFolder: String?,
+ fontFileExtension: String,
+ remappedFontPath: String?,
+) {
+ val path = remappedFontPath ?: "$fontAssetsFolder${font.family}${fontFileExtension}"
+ val typefaceWithDefaultStyle = try {
+ Typeface.createFromAsset(context.assets, path)
+ } catch (e: Exception) {
+ Logger.error("Failed to find typeface in assets with path $path.", e)
+ return
+ }
+ try {
+ val typefaceWithStyle = typefaceForStyle(typefaceWithDefaultStyle, font.style)
+ font.typeface = typefaceWithStyle
+ } catch (e: Exception) {
+ Logger.error("Failed to create ${font.family} typeface with style=${font.style}!", e)
+ }
+}
+
+private fun typefaceForStyle(typeface: Typeface, style: String): Typeface? {
+ val containsItalic = style.contains("Italic")
+ val containsBold = style.contains("Bold")
+ val styleInt = when {
+ containsItalic && containsBold -> Typeface.BOLD_ITALIC
+ containsItalic -> Typeface.ITALIC
+ containsBold -> Typeface.BOLD
+ else -> Typeface.NORMAL
+ }
+ return if (typeface.style == styleInt) typeface else Typeface.create(typeface, styleInt)
+}
+
private fun String?.ensureTrailingSlash(): String? = when {
- this == null -> null
+ isNullOrBlank() -> null
endsWith('/') -> this
else -> "$this/"
+}
+
+private fun String.ensureLeadingPeriod(): String = when {
+ isBlank() -> this
+ startsWith(".") -> this
+ else -> ".$this"
}
\ No newline at end of file
diff --git a/lottie/src/main/java/com/airbnb/lottie/LottieProperty.java b/lottie/src/main/java/com/airbnb/lottie/LottieProperty.java
index 98dea45..0686156 100644
--- a/lottie/src/main/java/com/airbnb/lottie/LottieProperty.java
+++ b/lottie/src/main/java/com/airbnb/lottie/LottieProperty.java
@@ -2,6 +2,7 @@
import android.graphics.ColorFilter;
import android.graphics.PointF;
+import android.graphics.Typeface;
import com.airbnb.lottie.value.LottieValueCallback;
import com.airbnb.lottie.value.ScaleXY;
@@ -164,4 +165,6 @@
ColorFilter COLOR_FILTER = new ColorFilter();
Integer[] GRADIENT_COLOR = new Integer[0];
+
+ Typeface TYPEFACE = Typeface.DEFAULT;
}
diff --git a/lottie/src/main/java/com/airbnb/lottie/model/Font.java b/lottie/src/main/java/com/airbnb/lottie/model/Font.java
index 9c74ff0..5aa3d45 100644
--- a/lottie/src/main/java/com/airbnb/lottie/model/Font.java
+++ b/lottie/src/main/java/com/airbnb/lottie/model/Font.java
@@ -2,6 +2,9 @@
import static androidx.annotation.RestrictTo.Scope.LIBRARY;
+import android.graphics.Typeface;
+
+import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
@RestrictTo(LIBRARY)
@@ -12,6 +15,9 @@
private final String style;
private final float ascent;
+ @Nullable
+ private Typeface typeface;
+
public Font(String family, String name, String style, float ascent) {
this.family = family;
this.name = name;
@@ -34,4 +40,13 @@
@SuppressWarnings("unused") float getAscent() {
return ascent;
}
+
+ @Nullable
+ public Typeface getTypeface() {
+ return typeface;
+ }
+
+ public void setTypeface(@Nullable Typeface typeface) {
+ this.typeface = typeface;
+ }
}
diff --git a/lottie/src/main/java/com/airbnb/lottie/model/layer/TextLayer.java b/lottie/src/main/java/com/airbnb/lottie/model/layer/TextLayer.java
index d15c537..b0f3b2d 100644
--- a/lottie/src/main/java/com/airbnb/lottie/model/layer/TextLayer.java
+++ b/lottie/src/main/java/com/airbnb/lottie/model/layer/TextLayer.java
@@ -47,7 +47,7 @@
setStyle(Style.STROKE);
}};
private final Map<FontCharacter, List<ContentGroup>> contentsForCharacter = new HashMap<>();
- private final LongSparseArray<String> codePointCache = new LongSparseArray<String>();
+ private final LongSparseArray<String> codePointCache = new LongSparseArray<>();
private final TextKeyframeAnimation textAnimation;
private final LottieDrawable lottieDrawable;
private final LottieComposition composition;
@@ -71,6 +71,8 @@
private BaseKeyframeAnimation<Float, Float> textSizeAnimation;
@Nullable
private BaseKeyframeAnimation<Float, Float> textSizeCallbackAnimation;
+ @Nullable
+ private BaseKeyframeAnimation<Typeface, Typeface> typefaceCallbackAnimation;
TextLayer(LottieDrawable lottieDrawable, Layer layerModel) {
super(lottieDrawable, layerModel);
@@ -236,8 +238,7 @@
private void drawTextWithFont(
DocumentData documentData, Font font, Matrix parentMatrix, Canvas canvas) {
- float parentScale = Utils.getScale(parentMatrix);
- Typeface typeface = lottieDrawable.getTypeface(font.getFamily(), font.getStyle());
+ Typeface typeface = getTypeface(font);
if (typeface == null) {
return;
}
@@ -298,6 +299,21 @@
}
}
+ @Nullable
+ private Typeface getTypeface(Font font) {
+ if (typefaceCallbackAnimation != null) {
+ Typeface callbackTypeface = typefaceCallbackAnimation.getValue();
+ if (callbackTypeface != null) {
+ return callbackTypeface;
+ }
+ }
+ Typeface drawableTypeface = lottieDrawable.getTypeface(font.getFamily(), font.getStyle());
+ if (drawableTypeface != null) {
+ return drawableTypeface;
+ }
+ return font.getTypeface();
+ }
+
private List<String> getTextLines(String text) {
// Split full text by carriage return character
String formattedText = text.replaceAll("\r\n", "\r")
@@ -517,6 +533,18 @@
textSizeCallbackAnimation.addUpdateListener(this);
addAnimation(textSizeCallbackAnimation);
}
+ } else if (property == LottieProperty.TYPEFACE) {
+ if (typefaceCallbackAnimation != null) {
+ removeAnimation(typefaceCallbackAnimation);
+ }
+
+ if (callback == null) {
+ typefaceCallbackAnimation = null;
+ } else {
+ typefaceCallbackAnimation = new ValueCallbackKeyframeAnimation<>((LottieValueCallback<Typeface>) callback);
+ typefaceCallbackAnimation.addUpdateListener(this);
+ addAnimation(typefaceCallbackAnimation);
+ }
}
}
}
diff --git a/sample-compose/src/main/assets/fonts/Comic Neue.ttf b/sample-compose/src/main/assets/fonts/Comic Neue.ttf
new file mode 100755
index 0000000..ab3a417
--- /dev/null
+++ b/sample-compose/src/main/assets/fonts/Comic Neue.ttf
Binary files differ
diff --git a/sample-compose/src/main/assets/fonts/Roboto.ttf b/sample-compose/src/main/assets/fonts/Roboto.ttf
new file mode 100755
index 0000000..2c97eea
--- /dev/null
+++ b/sample-compose/src/main/assets/fonts/Roboto.ttf
Binary files differ
diff --git a/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/ComposeActivity.kt b/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/ComposeActivity.kt
index 9b12b5f..af9dd05 100644
--- a/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/ComposeActivity.kt
+++ b/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/ComposeActivity.kt
@@ -30,6 +30,7 @@
import com.airbnb.lottie.sample.compose.examples.ExamplesPage
import com.airbnb.lottie.sample.compose.examples.ImagesExamplesPage
import com.airbnb.lottie.sample.compose.examples.NetworkExamplesPage
+import com.airbnb.lottie.sample.compose.examples.TextExamplesPage
import com.airbnb.lottie.sample.compose.examples.TransitionsExamplesPage
import com.airbnb.lottie.sample.compose.examples.ViewPagerExamplePage
import com.airbnb.lottie.sample.compose.lottiefiles.LottieFilesPage
@@ -100,6 +101,7 @@
composable(Route.NetworkExamples.route) { NetworkExamplesPage() }
composable(Route.DynamicPropertiesExamples.route) { DynamicPropertiesExamplesPage() }
composable(Route.ImagesExamples.route) { ImagesExamplesPage() }
+ composable(Route.TextExamples.route) { TextExamplesPage() }
composable(
Route.Player.fullRoute,
arguments = Route.Player.args
diff --git a/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/Route.kt b/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/Route.kt
index 719d219..9c8ee0e 100644
--- a/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/Route.kt
+++ b/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/Route.kt
@@ -28,6 +28,8 @@
object ImagesExamples : Route("image examples")
+ object TextExamples : Route("text examples")
+
object DynamicPropertiesExamples : Route("dynamic properties examples")
object Player : Route(
diff --git a/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/examples/ExamplesPage.kt b/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/examples/ExamplesPage.kt
index bb85651..b4c4402 100644
--- a/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/examples/ExamplesPage.kt
+++ b/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/examples/ExamplesPage.kt
@@ -64,5 +64,11 @@
modifier = Modifier
.clickable { navController.navigate(Route.ImagesExamples) }
)
+ ListItem(
+ text = { Text("Text") },
+ secondaryText = { Text("Using animations with text") },
+ modifier = Modifier
+ .clickable { navController.navigate(Route.TextExamples) }
+ )
}
}
\ No newline at end of file
diff --git a/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/examples/TextExamplesPage.kt b/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/examples/TextExamplesPage.kt
new file mode 100644
index 0000000..52a0140
--- /dev/null
+++ b/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/examples/TextExamplesPage.kt
@@ -0,0 +1,102 @@
+package com.airbnb.lottie.sample.compose.examples
+
+import android.graphics.Typeface
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import com.airbnb.lottie.LottieProperty
+import com.airbnb.lottie.compose.LottieAnimation
+import com.airbnb.lottie.compose.LottieCompositionSpec
+import com.airbnb.lottie.compose.rememberLottieComposition
+import com.airbnb.lottie.compose.rememberLottieDynamicProperties
+import com.airbnb.lottie.compose.rememberLottieDynamicProperty
+import com.airbnb.lottie.sample.compose.R
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+
+@Composable
+fun TextExamplesPage() {
+ UsageExamplePageScaffold {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .verticalScroll(rememberScrollState())
+ ) {
+ ExampleCard("Default", "Loading fonts using default asset paths") {
+ Example1()
+ }
+ ExampleCard("Font Remapping", "Replace fonts using font remapping") {
+ Example2()
+ }
+ ExampleCard("Dynamic Properties", "Replace fonts with custom typefaces") {
+ Example3()
+ }
+ }
+ }
+}
+
+@Composable
+private fun Example1() {
+ // Lottie will automatically look for fonts in src/main/assets/fonts.
+ // It will find font files based on the font family specified in the Lottie Json file.
+ // You can specify a different assets subfolder by using the fontAssetsFolder parameter.
+ // By default, it will look for ttf files.
+ // You can specify a different file extension by using the fontFileExtension parameter.
+ val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.name))
+ LottieAnimation(
+ composition,
+ progress = 0f,
+ )
+}
+
+@Composable
+private fun Example2() {
+ val composition by rememberLottieComposition(
+ LottieCompositionSpec.RawRes(R.raw.name),
+ // Comic Neue is the font family set in the Lottie json file.
+ fontRemapping = mapOf("Comic Neue" to "fonts/Roboto.ttf"),
+ // Don't cache the composition because it has a custom font remapping.
+ cacheComposition = false,
+ )
+ LottieAnimation(
+ composition,
+ progress = 0f,
+ )
+}
+
+@Composable
+private fun Example3() {
+ val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.name))
+ val typeface = rememberTypeface("fonts/Roboto.ttf")
+ val dynamicProperties = rememberLottieDynamicProperties(
+ rememberLottieDynamicProperty(LottieProperty.TYPEFACE, typeface, "NAME")
+ )
+
+ LottieAnimation(
+ composition,
+ progress = 0f,
+ dynamicProperties = dynamicProperties,
+ )
+}
+
+@Composable
+private fun rememberTypeface(path: String): Typeface? {
+ var typeface: Typeface? by remember { mutableStateOf(null) }
+ val context = LocalContext.current
+ LaunchedEffect(path) {
+ typeface = null
+ withContext(Dispatchers.IO) {
+ typeface = Typeface.createFromAsset(context.assets, "fonts/Roboto.ttf")
+ }
+ }
+ return typeface
+}
\ No newline at end of file
diff --git a/sample-compose/src/main/res/raw/name.json b/sample-compose/src/main/res/raw/name.json
new file mode 100644
index 0000000..fad053c
--- /dev/null
+++ b/sample-compose/src/main/res/raw/name.json
@@ -0,0 +1,2 @@
+{"v":"4.8.0","fr":29.9700012207031,"ip":0,"op":61.0000024845809,"w":150,"h":150,"nm":"Name",
+ "ddd":0,"assets":[],"fonts":{"list":[{"origin":0,"fPath":"","fClass":"","fFamily":"Comic Neue","fWeight":"","fStyle":"Regular","fName":"ComicNeue","ascent":69.6990966796875}]},"layers":[{"ddd":0,"ind":1,"ty":5,"nm":"NAME","ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":0,"s":[10.5,15,0],"e":[10.5,147,0],"to":[0,22,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":15,"s":[10.5,147,0],"e":[10.5,15,0],"to":[0,0,0],"ti":[0,22,0]},{"t":30.0000012219251}]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":0,"s":[100,100,100],"e":[196,196,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":15,"s":[196,196,100],"e":[100,100,100]},{"t":30.0000012219251}]}},"ao":0,"t":{"d":{"k":[{"s":{"s":14,"f":"ComicNeue","t":"NAME","j":0,"tr":0,"lh":16.8,"ls":0,"fc":[0.92,0,0]},"t":0}]},"p":{},"m":{"g":1,"a":{"a":0,"k":[0,0]}},"a":[{"s":{"t":0,"xe":{"a":0,"k":0},"ne":{"a":0,"k":0},"a":{"a":0,"k":100},"b":1,"rn":0,"sh":1,"r":1},"a":{"fc":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":0,"s":[1,0,0,1],"e":[0,1,0,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":15,"s":[0,1,0,1],"e":[0,0,1,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":30,"s":[0,0,1,1],"e":[0,1,0,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":45,"s":[0,1,0,1],"e":[1,0,0,1]},{"t":60.0000024438501}]},"t":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":30,"s":[0],"e":[20]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":45,"s":[20],"e":[0]},{"t":60.0000024438501}]}}}]},"ip":0,"op":61.0000024845809,"st":0,"bm":0,"sr":1}]}
\ No newline at end of file