{"id":3209,"date":"2023-05-11T10:54:59","date_gmt":"2023-05-11T17:54:59","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/surface-duo\/?p=3209"},"modified":"2023-05-11T10:54:59","modified_gmt":"2023-05-11T17:54:59","slug":"jetpack-compose-animation-1","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/surface-duo\/jetpack-compose-animation-1\/","title":{"rendered":"Budding love for compose animation"},"content":{"rendered":"<p>\n  Hello Jetpack Compose developers,\n<\/p>\n<p>\n  Last week, we did some Compose animation work to <a href=\"https:\/\/devblogs.microsoft.com\/surface-duo\/android-openai-chatgpt-6\/\">build a typing indicator in a chat app<\/a>. For our next blog series, I\u2019ll be diving into more animation topics with a project inspired by <a href=\"https:\/\/www.droidcon.com\/2022\/08\/01\/composable-sheep-a-compose-animations-journey\/\">Nicole Terc\u2019s Composable Sheep talk<\/a> from droidcon NYC. Her composable sheep talk series inspired me to work on my own colorful and playful animated garden project while learning Compose Multiplatform!\n<\/p>\n<h2>Inspiration<\/h2>\n<p>\n  For those of you who haven\u2019t had a chance yet, I\u2019d highly recommend watching the <a href=\"https:\/\/www.droidcon.com\/2022\/08\/01\/composable-sheep-a-compose-animations-journey\/\">original Compose Sheep talk<\/a>. There\u2019s also a <a href=\"https:\/\/www.droidcon.com\/2022\/11\/15\/composable-sheep-the-creative-coding-epilogue\/\">Composable Sheep sequel<\/a> from droidcon London, where Nicole and Tasha partner up to show us some even cooler animation (and mathematical) concepts.\n<\/p>\n<p>\n  I used the sample repositories from these talks as reference for my own investigations:\n<\/p>\n<ul>\n<li><a href=\"https:\/\/github.com\/nicole-terc\/composable-sheep\">nicole-terc\/composable-sheep (github.com)<\/a>\n  <\/li>\n<li><a href=\"https:\/\/github.com\/drinkthestars\/composable-sheep-sketches\">drinkthestars\/composable-sheep-sketches: Funky composable sheep \ud83d\ude0e \ud83d\udc0f (github.com)<\/a>\n  <\/li>\n<\/ul>\n<p>\n  Like the original sheep project, we\u2019ll be using the Compose Graphics libraries (ex: <code>Canvas<\/code>, <code>DrawScope<\/code>\u2026) and animation APIs to draw our projects frame by frame. All of the code covered today can be referenced in the <a href=\"https:\/\/github.com\/khalp\/animated-garden\/pull\/1\">Animated garden part 1 PR<\/a>.\n<\/p>\n<h2>Draw a flower<\/h2>\n<p>\n  In order to start growing my garden, the first step was to figure out how to draw a flower. In <code>DrawScope<\/code>, you have access to many different functions for drawing text, shapes, and images.\n<\/p>\n<p>\n  To build a basic flower, we need two basic parts: the petals and the center of the flower. To draw these, we\u2019ll use the <code>DrawScope<\/code> methods <a href=\"https:\/\/developer.android.com\/reference\/kotlin\/androidx\/compose\/ui\/graphics\/drawscope\/DrawScope#drawOval(androidx.compose.ui.graphics.Color,androidx.compose.ui.geometry.Offset,androidx.compose.ui.geometry.Size,kotlin.Float,androidx.compose.ui.graphics.drawscope.DrawStyle,androidx.compose.ui.graphics.ColorFilter,androidx.compose.ui.graphics.BlendMode)\"><code>drawOval<\/code><\/a> and <a href=\"https:\/\/developer.android.com\/reference\/kotlin\/androidx\/compose\/ui\/graphics\/drawscope\/DrawScope#drawCircle(androidx.compose.ui.graphics.Color,kotlin.Float,androidx.compose.ui.geometry.Offset,kotlin.Float,androidx.compose.ui.graphics.drawscope.DrawStyle,androidx.compose.ui.graphics.ColorFilter,androidx.compose.ui.graphics.BlendMode)\"><code>drawCircle<\/code><\/a>.\n<\/p>\n<p>\n  After some experimentation, I found the ideal sizes and colors for the first flower in my garden \u2013 a sunflower \ud83c\udf3b! The code to draw this flower is simple:\n<\/p>\n<pre>  private val <em>Brown <\/em>= <em>Color<\/em>(0xFF742C0D)\r\n  private val <em>Yellow <\/em>= <em>Color<\/em>(0xFFF8D314)\r\n\r\n  @Composable\r\n  fun Sunflower() {\r\n      <em>Canvas<\/em>(Modifier.<em>size<\/em>(100.<em>dp<\/em>)) {\r\n        <em>drawSunflower<\/em>()\r\n      }\r\n  }\r\n\r\n  fun DrawScope.drawSunflower() {\r\n      <em>drawPetals<\/em>()\r\n      <em>drawCenter<\/em>()\r\n  }\r\n\r\n  fun DrawScope.drawCenter() {\r\n      val radius = 20f\r\n\r\n      drawCircle(color = <em>Brown<\/em>, radius = radius, center = center)\r\n  }\r\n  fun DrawScope.drawPetals() {\r\n      val numPetals = 8\r\n      var angle = 0f\r\n      val size = <em>Size<\/em>(width = 30f, height = 75f)\r\n\r\n      <em>repeat<\/em>(numPetals) <strong>{<br \/>        <\/strong><em>rotate<\/em>(angle) <strong>{\r\n            <\/strong>drawOval(\r\n                  color = <em>Yellow<\/em>,\r\n                  topLeft = <em>Offset<\/em>(center.x - size.width \/ 2, center.y),\r\n                  size = size\r\n              )\r\n          <strong>}<br \/>        <\/strong>angle += 360 \/ numPetals\r\n      <strong>}<br \/><\/strong>\r\n  }<\/pre>\n<p>\n  Some subtle but important details from the code snippet:\n<\/p>\n<ul>\n<li>\n    When using <code>Canvas<\/code>, you <em>must<\/em> provide a size modifier, otherwise it will default to zero and your drawings will not be shown\n  <\/li>\n<li>\n    Order matters in drawing operations \u2013 since we want the center shown above the petals, we have to make sure to draw it last\n  <\/li>\n<li>\n    In <code>DrawScope<\/code>, you gain access to fields like <code>center<\/code> and <code>size<\/code> that are really helpful for making calculations\n  <\/li>\n<\/ul>\n<p>\n  No respectable garden has only one flower, so of course we have to add some code to grow the garden:\n<\/p>\n<pre>  @Composable\r\n  fun MyGarden() {\r\n      <em>LazyVerticalGrid<\/em>(\r\n          modifier = Modifier.<em>fillMaxSize<\/em>(),\r\n          columns = GridCells.Adaptive(100.<em>dp<\/em>)\r\n      ) {\r\n        items(8) {\r\n            <em>Sunflower<\/em>()\r\n          }\r\n      }\r\n  }\r\n<\/pre>\n<p>\n  And with that, we have a nice basic garden full of sunflowers \ud83d\ude0a\n<\/p>\n<p>\n  <img decoding=\"async\" width=\"865\" height=\"403\" src=\"https:\/\/devblogs.microsoft.com\/surface-duo\/wp-content\/uploads\/sites\/53\/2023\/05\/word-image-3209-2.png\" class=\"wp-image-3211\" srcset=\"https:\/\/devblogs.microsoft.com\/surface-duo\/wp-content\/uploads\/sites\/53\/2023\/05\/word-image-3209-2.png 865w, https:\/\/devblogs.microsoft.com\/surface-duo\/wp-content\/uploads\/sites\/53\/2023\/05\/word-image-3209-2-300x140.png 300w, https:\/\/devblogs.microsoft.com\/surface-duo\/wp-content\/uploads\/sites\/53\/2023\/05\/word-image-3209-2-768x358.png 768w\" sizes=\"(max-width: 865px) 100vw, 865px\" \/>\n<\/p>\n<h2>Add an animation canvas<\/h2>\n<p>\n  The next step to beautifying our garden is adding some animations. To set up our animations, the first thing we need to do is copy over a simplified version of the <a href=\"https:\/\/github.com\/drinkthestars\/composable-sheep-sketches\/blob\/main\/canvasSketch\/src\/main\/java\/com\/canvas\/sketch\/Sketch.kt#L157\"><code>Sketch<\/code> composable<\/a> from the composable-sheep-sketches repo:\n<\/p>\n<pre>  @Composable\r\n  fun Sketch(\r\n      modifier: Modifier = Modifier,\r\n      targetValue: Float,\r\n      animationSpec: AnimationSpec&lt;Float&gt;,\r\n      onDraw: DrawScope.(Float) -&gt; Unit\r\n  ) {\r\n      val animationState = <em>remember <\/em>{ <em>AnimationState<\/em>(0f) }\r\n\r\n    <em>LaunchedEffect<\/em>(targetValue) {\r\n        while (<em>isActive<\/em>) {\r\n              animationState.animateTo(\r\n                  targetValue = targetValue,\r\n                  animationSpec = animationSpec,\r\n                  sequentialAnimation = true\r\n              )\r\n          }\r\n      }\r\n    <em>Canvas<\/em>(modifier = modifier) {\r\n        onDraw(animationState.value)\r\n      <strong>}<br \/><\/strong>\r\n  }<\/pre>\n<p>\n  This will allow us to draw our animations frame by frame. Now, instead of wrapping our drawings with <code>Canvas<\/code>, we\u2019ll use <code>Sketch<\/code>, and in the <code>onDraw<\/code> method you can see that you\u2019ll have access to the <code>animationState<\/code> value. The <code>targetValue<\/code> and <code>animationSpec<\/code> parameters will allow you to customize the speed and style of animation, as you will see in later examples.\n<\/p>\n<h2>Build a basic animation<\/h2>\n<p>\n  Now that we have the animation canvas set up with the <code>Sketch<\/code> composable, we can start to build some fun animations for our garden.\n<\/p>\n<p>\n  In all basic animations, we\u2019re essentially choosing one property to change over time. Once you\u2019ve decided on a property, you can figure out what values you want to animate in between, how quickly the value should change, if the animation should repeat, etc.\n<\/p>\n<p>\n  For our first basic flower animation, we\u2019ll be trying to animate the size of the flowers. To do that, we\u2019ll have to:\n<\/p>\n<ul>\n<li>\n    Update the flower drawing logic\n  <\/li>\n<li>\n    Connect the <code>drawSunflower<\/code> method to the animation canvas\n  <\/li>\n<\/ul>\n<h3>Update drawing logic<\/h3>\n<p>\n  To change how big the flowers are drawn, we\u2019ll have to change the code in two places:\n<\/p>\n<ul>\n<li>\n    The <code>radius<\/code> parameter when we call <code>drawCircle<\/code> for the center of the flower\n  <\/li>\n<li>\n    The <code>size<\/code> parameter when we call <code>drawOval<\/code> for the petals of the flower\n  <\/li>\n<\/ul>\n<p>\n  To ensure that the center and petals grow at the same rate, we can describe the size as a percentage instead of a specific value. This way, we won\u2019t need to keep track of two different size values and we can easily create an animation state that varies between <code>0f<\/code> and <code>1f<\/code>.\n<\/p>\n<p>\n  I like the size of the basic sunflowers from the previous section, so let\u2019s say we want to animate between zero up to the hardcoded sizes from earlier. To achieve that, the updated <code>drawSunflower<\/code> method would look like this:\n<\/p>\n<pre>  fun DrawScope.drawSunflower(sizePct: Float) {\r\n      <em>drawPetals<\/em>(sizePct)\r\n      <em>drawCenter<\/em>(sizePct)\r\n  }\r\n\r\n  fun DrawScope.drawCenter(sizePct: Float) {\r\n      val radius = 20f * sizePct\r\n\r\n      drawCircle(color = <em>Brown<\/em>, radius = radius, center = center)\r\n  }\r\n\r\n  fun DrawScope.drawPetals(sizePct: Float) {\r\n      val numPetals = 8\r\n      var angle = 0f\r\n      val size = <em>Size<\/em>(width = 30f, height = 75f) * sizePct\r\n\r\n      <em>repeat<\/em>(numPetals) {\r\n        <em>rotate<\/em>(angle) {\r\n            drawOval(\r\n                  color = <em>Yellow<\/em>,\r\n                  topLeft = <em>Offset<\/em>(center.x - size.width \/ 2, center.y),\r\n                  size = size\r\n              )\r\n          }\r\n        angle += 360 \/ numPetals\r\n      }\r\n  }<\/pre>\n<p>\n  You can see the only differences are that we now have the <code>sizePct<\/code> float parameter, which will be our animation state that changes between <code>0f<\/code> and <code>1f<\/code>, and that we multiply the <code>radius<\/code> and <code>size<\/code> values by this percentage.\n<\/p>\n<h3>Connect to animation canvas<\/h3>\n<p>\n   The last step is to animate our sunflowers is to call <code>drawSunflower<\/code> inside our new <code>Sketch<\/code> animation canvas and pass along the animation state.\n<\/p>\n<p>\n  Let\u2019s take a look at the <code>Sketch<\/code> composable\u2019s parameters:\n<\/p>\n<pre>  @Composable\r\n  fun Sketch(\r\n      modifier: Modifier = Modifier,\r\n      targetValue: Float,\r\n      animationSpec: AnimationSpec&lt;Float&gt;,\r\n      onDraw: DrawScope.(Float) -&gt; Unit\r\n  )<\/pre>\n<p>\n  We have some typical composable parameters, including a modifier and a content parameter (<code>onDraw<\/code>) with the <code>DrawScope<\/code> receiver and <code>Float<\/code> parameter that will give us access to the animation state.\n<\/p>\n<p>\n  The <code>targetValue<\/code> parameter controls the end value of the animation state (the start value is always <code>0f<\/code> in our examples). For our <code>sizePct<\/code> animation state, we want the animation state to vary between <code>0f<\/code> and <code>1f<\/code>, so we\u2019ll set <code>targetValue<\/code> to <code>1f<\/code>.\n<\/p>\n<p>\n  The <code>animationSpec<\/code> parameter lets you customize animation behavior, such as how quickly the animation state changes over time, if it repeats, etc. You can read more about the different options in the <a href=\"https:\/\/developer.android.com\/jetpack\/compose\/animation\/customize\">Customize animations documentation<\/a>, but for our purposes, we\u2019ll be using <a href=\"https:\/\/developer.android.com\/jetpack\/compose\/animation\/customize#infiniterepeatable\"><code>infiniteRepeatable<\/code><\/a> to infinitely loop our animation and <a href=\"https:\/\/developer.android.com\/jetpack\/compose\/animation\/customize#tween\"><code>tween<\/code><\/a> to describe our animation behavior.\n<\/p>\n<p>\n  Both functions accept three parameters:\n<\/p>\n<pre>  @Stable\r\n  fun &lt;T&gt; infiniteRepeatable(\r\n      animation: DurationBasedAnimationSpec&lt;T&gt;,\r\n      repeatMode: RepeatMode = RepeatMode.<em>Restart<\/em>,\r\n      initialStartOffset: StartOffset = StartOffset(0)\r\n  ): InfiniteRepeatableSpec&lt;T&gt;\r\n\r\n  @Stable\r\n  fun &lt;T&gt; tween(\r\n      durationMillis: Int = <em>DefaultDurationMillis<\/em>,\r\n      delayMillis: Int = 0,\r\n      easing: Easing = <em>FastOutSlowInEasing<br \/><\/em>\r\n  ): TweenSpec&lt;T&gt;<\/pre>\n<p>\n  Let\u2019s try calling these functions with their default behavior and see what the flower animation looks like:\n<\/p>\n<pre>  @Composable\r\n  fun AnimatedSizeSunflower() {\r\n      <em>Sketch<\/em>(\r\n          modifier = Modifier.<em>size<\/em>(100.<em>dp<\/em>),\r\n          targetValue = 1f,\r\n          animationSpec = <em>infiniteRepeatable<\/em>(animation = <em>tween<\/em>())\r\n      ) <strong>{ <\/strong>sizePct <strong>-&gt;<br \/>        <\/strong><em>drawSunflower<\/em>(sizePct = sizePct)\r\n      }\r\n  }<\/pre>\n<p>\n  <img decoding=\"async\" width=\"80\" height=\"80\" src=\"https:\/\/devblogs.microsoft.com\/surface-duo\/wp-content\/uploads\/sites\/53\/2023\/05\/word-image-3209-3.gif\" class=\"wp-image-3212\" srcset=\"https:\/\/devblogs.microsoft.com\/surface-duo\/wp-content\/uploads\/sites\/53\/2023\/05\/word-image-3209-3.gif 80w, https:\/\/devblogs.microsoft.com\/surface-duo\/wp-content\/uploads\/sites\/53\/2023\/05\/word-image-3209-3-24x24.gif 24w, https:\/\/devblogs.microsoft.com\/surface-duo\/wp-content\/uploads\/sites\/53\/2023\/05\/word-image-3209-3-48x48.gif 48w\" sizes=\"(max-width: 80px) 100vw, 80px\" \/>\n<\/p>\n<p>\n  Not bad! However, I have a few tweaks I want to make:\n<\/p>\n<ol>\n<li>\n  Slow down the animation<\/p>\n<p>\n  To do this, we\u2019ll update the <code>tween<\/code> call with a longer <code>durationMillis<\/code> value.\n<\/p>\n<\/li>\n<li>\n  Have the flower shrink as well as grow<\/p>\n<p>\n  We can update the <code>infiniteRepeatable<\/code> call by setting the <code>repeatMode<\/code> to <code>RepeatMode.Reverse<\/code>. That way, the flower will grow, then shrink, instead of restarting the growing animation each time.\n<\/p>\n<\/li>\n<li>\n  Add a pause in between growing and shrinking<\/p>\n<p>\n  We can also address this by updating the <code>tween<\/code> call, this time by setting the <code>delayMillis<\/code> parameter to a non-zero value. This will add a delay before the animation starts.\n<\/p>\n<\/li>\n<li>\n  Change the rate at which the flower grows and shrinks<\/p>\n<p>\n  For this, we\u2019ll investigate different values for the <code>easing<\/code> parameter in the <code>tween<\/code> call. There are <a href=\"https:\/\/developer.android.com\/jetpack\/compose\/animation\/customize#easing\">many easing options<\/a> available for use, and you can experiment with them (and read up on <a href=\"https:\/\/m2.material.io\/design\/motion\/speed.html#easing\">Material Design<\/a>) to find out which one works best for your use case.\n<\/p>\n<\/li>\n<\/ol>\n<p>\n  After some experimentation, I settled on the following values for our animation spec:\n<\/p>\n<pre>  @Composable\r\n  fun AnimatedSizeSunflower() {\r\n      <em>Sketch<\/em>(\r\n          modifier = Modifier.<em>size<\/em>(100.<em>dp<\/em>),\r\n          targetValue = 1f,\r\n          animationSpec = <em>infiniteRepeatable<\/em>(\r\n              animation = <em>tween<\/em>(durationMillis = 800, delayMillis = 50, easing = <em>LinearOutSlowInEasing<\/em>),\r\n              repeatMode = RepeatMode.<em>Reverse<br \/>        <\/em>)\r\n      ) { sizePct -&gt;\r\n        <em>drawSunflower<\/em>(sizePct = sizePct)\r\n      }\r\n  }<\/pre>\n<p>\n  Take a look at the garden now:\n<\/p>\n<p>\n  <img decoding=\"async\" width=\"600\" height=\"265\" src=\"https:\/\/devblogs.microsoft.com\/surface-duo\/wp-content\/uploads\/sites\/53\/2023\/05\/word-image-3209-4.gif\" class=\"wp-image-3213\" \/>\n<\/p>\n<p>\n  So now, you know more about graphics in Compose, creating a frame-by-frame animation canvas, and building basic animations! Next week, we\u2019ll dive into animating more flower properties (hint: \ud83c\udf08) to make our garden even cooler \ud83d\ude0e\n<\/p>\n<h2>Resources and feedback<\/h2>\n<p>\n  The content covered today is part of <a href=\"https:\/\/github.com\/khalp\/animated-garden\/pull\/1\">PR #1<\/a> in the <a href=\"https:\/\/github.com\/khalp\/animated-garden\">animated-garden repo<\/a>. \n<\/p>\n<p>\n  To learn more about Jetpack Compose animations and creative coding, check out these resources:\n<\/p>\n<ul>\n<li><a href=\"https:\/\/github.com\/nicole-terc\/composable-sheep\">nicole-terc\/composable-sheep (github.com)<\/a>\n  <\/li>\n<li><a href=\"https:\/\/github.com\/drinkthestars\/composable-sheep-sketches\">drinkthestars\/composable-sheep-sketches: Funky composable sheep \ud83d\ude0e \ud83d\udc0f (github.com)<\/a>\n  <\/li>\n<li><a href=\"https:\/\/www.droidcon.com\/2022\/08\/01\/composable-sheep-a-compose-animations-journey\/\">Composable Sheep &#8211; A Compose Animations Journey<\/a>\n  <\/li>\n<li><a href=\"https:\/\/www.droidcon.com\/2022\/11\/15\/composable-sheep-the-creative-coding-epilogue\/\">Composable Sheep &#8211; The Creative Coding Epilogue<\/a>\n  <\/li>\n<li><a href=\"https:\/\/developer.android.com\/jetpack\/compose\/graphics\/draw\/overview\">Graphics in Jetpack Compose<\/a>\n  <\/li>\n<li><a href=\"https:\/\/developer.android.com\/jetpack\/compose\/animation\">Animations in Jetpack Compose<\/a>\n  <\/li>\n<li><a href=\"https:\/\/developer.android.com\/jetpack\/compose\/animation\/customize\">Customize animations in Jetpack Compose<\/a>\n  <\/li>\n<\/ul>\n<p>\n  If you have any questions, use the <a href=\"http:\/\/aka.ms\/SurfaceDuoSDK-Feedback\">feedback forum<\/a> or message us on <a href=\"https:\/\/twitter.com\/surfaceduodev\">Twitter @surfaceduodev<\/a>.\n<\/p>\n<p>\n  We won\u2019t be livestreaming this week, but you can check out the <a href=\"https:\/\/youtube.com\/c\/surfaceduodev\">archives on YouTube<\/a>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Hello Jetpack Compose developers, Last week, we did some Compose animation work to build a typing indicator in a chat app. For our next blog series, I\u2019ll be diving into more animation topics with a project inspired by Nicole Terc\u2019s Composable Sheep talk from droidcon NYC. Her composable sheep talk series inspired me to work [&hellip;]<\/p>\n","protected":false},"author":72597,"featured_media":3210,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[1],"tags":[736,719,692],"class_list":["post-3209","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-surface-duo-sdk","tag-animation","tag-droidcon","tag-jetpack-compose"],"acf":[],"blog_post_summary":"<p>Hello Jetpack Compose developers, Last week, we did some Compose animation work to build a typing indicator in a chat app. For our next blog series, I\u2019ll be diving into more animation topics with a project inspired by Nicole Terc\u2019s Composable Sheep talk from droidcon NYC. Her composable sheep talk series inspired me to work [&hellip;]<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/surface-duo\/wp-json\/wp\/v2\/posts\/3209","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/surface-duo\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/surface-duo\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/surface-duo\/wp-json\/wp\/v2\/users\/72597"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/surface-duo\/wp-json\/wp\/v2\/comments?post=3209"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/surface-duo\/wp-json\/wp\/v2\/posts\/3209\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/surface-duo\/wp-json\/wp\/v2\/media\/3210"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/surface-duo\/wp-json\/wp\/v2\/media?parent=3209"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/surface-duo\/wp-json\/wp\/v2\/categories?post=3209"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/surface-duo\/wp-json\/wp\/v2\/tags?post=3209"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}