Last active
April 22, 2024 01:48
-
-
Save JunkFood02/bf11191916ce200a9473e96ebfd07ec6 to your computer and use it in GitHub Desktop.
Container transform for cards (π included)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@file:OptIn( | |
ExperimentalSharedTransitionApi::class, | |
ExperimentalMaterial3Api::class | |
) | |
package com.example.compose_debug | |
import android.util.Log | |
import androidx.activity.compose.BackHandler | |
import androidx.annotation.DrawableRes | |
import androidx.compose.animation.AnimatedContent | |
import androidx.compose.animation.AnimatedVisibilityScope | |
import androidx.compose.animation.ExperimentalSharedTransitionApi | |
import androidx.compose.animation.SharedTransitionLayout | |
import androidx.compose.animation.SharedTransitionScope | |
import androidx.compose.animation.SizeTransform | |
import androidx.compose.animation.core.AnimationVector4D | |
import androidx.compose.animation.core.ExperimentalAnimationSpecApi | |
import androidx.compose.animation.core.TwoWayConverter | |
import androidx.compose.animation.core.tween | |
import androidx.compose.animation.fadeIn | |
import androidx.compose.animation.fadeOut | |
import androidx.compose.animation.scaleIn | |
import androidx.compose.animation.scaleOut | |
import androidx.compose.animation.togetherWith | |
import androidx.compose.foundation.Image | |
import androidx.compose.foundation.layout.Arrangement | |
import androidx.compose.foundation.layout.Box | |
import androidx.compose.foundation.layout.Column | |
import androidx.compose.foundation.layout.PaddingValues | |
import androidx.compose.foundation.layout.Row | |
import androidx.compose.foundation.layout.Spacer | |
import androidx.compose.foundation.layout.fillMaxWidth | |
import androidx.compose.foundation.layout.height | |
import androidx.compose.foundation.layout.padding | |
import androidx.compose.foundation.layout.size | |
import androidx.compose.foundation.layout.statusBarsPadding | |
import androidx.compose.foundation.layout.width | |
import androidx.compose.foundation.lazy.LazyColumn | |
import androidx.compose.foundation.lazy.items | |
import androidx.compose.foundation.shape.CircleShape | |
import androidx.compose.material.icons.Icons | |
import androidx.compose.material.icons.outlined.ArrowBack | |
import androidx.compose.material.icons.outlined.MoreVert | |
import androidx.compose.material.icons.outlined.Search | |
import androidx.compose.material.icons.outlined.Star | |
import androidx.compose.material.icons.outlined.StarBorder | |
import androidx.compose.material3.ButtonDefaults | |
import androidx.compose.material3.Card | |
import androidx.compose.material3.CardDefaults | |
import androidx.compose.material3.ExperimentalMaterial3Api | |
import androidx.compose.material3.FilledIconButton | |
import androidx.compose.material3.FilledIconToggleButton | |
import androidx.compose.material3.FilledTonalButton | |
import androidx.compose.material3.Icon | |
import androidx.compose.material3.IconButton | |
import androidx.compose.material3.IconButtonDefaults | |
import androidx.compose.material3.IconToggleButton | |
import androidx.compose.material3.MaterialTheme | |
import androidx.compose.material3.Scaffold | |
import androidx.compose.material3.Surface | |
import androidx.compose.material3.Text | |
import androidx.compose.runtime.Composable | |
import androidx.compose.runtime.getValue | |
import androidx.compose.runtime.mutableStateOf | |
import androidx.compose.runtime.remember | |
import androidx.compose.runtime.setValue | |
import androidx.compose.ui.Alignment | |
import androidx.compose.ui.Modifier | |
import androidx.compose.ui.draw.clip | |
import androidx.compose.ui.geometry.Rect | |
import androidx.compose.ui.layout.ContentScale | |
import androidx.compose.ui.res.painterResource | |
import androidx.compose.ui.text.style.TextOverflow | |
import androidx.compose.ui.tooling.preview.Preview | |
import androidx.compose.ui.unit.dp | |
private val ConversationList = listOf( | |
Conversation( | |
"θ§ηͺη«", | |
"η³»η»εηΊ§", | |
"ζζδΈζοΌθΏδΈͺη³»η»ζ―欑εηΊ§ι½ιθ¦θΏδΉδΉ εοΌ", | |
lastReplyTime = "10 mins ago", | |
avatarResId = R.drawable.img | |
), | |
Conversation( | |
"γγ³", | |
"γγ¬γΌγΉγγ«γγΌ", | |
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\n\nAliquam faucibus purus in massa. Ultricies leo integer malesuada nunc. Vitae nunc sed velit dignissim sodales. Nullam vehicula ipsum a arcu cursus vitae congue mauris rhoncus.\n\nPellentesque habitant morbi tristique senectus et netus et malesuada fames. Id diam vel quam elementum pulvinar etiam non quam lacus. Nisi lacus sed viverra tellus in.\n\nNibh sed pulvinar proin gravida hendrerit. Lectus proin nibh nisl condimentum id venenatis a. Luctus accumsan tortor posuere ac ut consequat. Nibh cras pulvinar mattis nunc sed blandit libero volutpat sed. Ac ut consequat semper viverra nam libero justo laoreet. Lectus nulla at volutpat diam ut venenatis tellus in. Nunc consequat interdum varius sit amet mattis vulputate enim.", | |
lastReplyTime = "20 mins ago", | |
avatarResId = R.drawable.img_4 | |
), | |
Conversation( | |
"Algorithm Kitty", | |
"Leetcode is hard", | |
"The good news is that LeetCode, like any skill, gets better with focused practice. Here's some advice:\n" + | |
"\n" + | |
"Start with the Basics: If you're new, focus on building a strong foundation in data structures and algorithms. Many resources are available for that.\n" + | |
"Solve Easier Problems First: Start with \"Easy\" problems on LeetCode and gradually work your way up. Celebrate those wins!\n" + | |
"Don't Just Memorize: Try to understand the underlying concepts behind the solutions. Can you explain it to yourself?\n" + | |
"Think Out Loud: When stuck, talk through your thought process step by step. Sometimes it helps clarify where your understanding falters.\n" + | |
"Look at Solutions: Don't be afraid to analyze solutions others have provided after you've given a problem your honest effort. This helps you learn new approaches.", | |
lastReplyTime = "2 days ago", | |
avatarResId = R.drawable.img_1, | |
isFavorite = true | |
), | |
) | |
@Composable | |
@Preview | |
fun ConversationContainerTransformDemo() { | |
var show by remember { | |
mutableStateOf(false) | |
} | |
var conversation: Conversation by remember { | |
mutableStateOf(ConversationList[1]) | |
} | |
Surface(color = MaterialTheme.colorScheme.surfaceContainer) { | |
SharedTransitionLayout { | |
AnimatedContent( | |
transitionSpec = { | |
fadeIn( | |
tween( | |
durationMillis = DURATION_ENTER, | |
delayMillis = DURATION_EXIT_SHORT, | |
easing = EmphasizedDecelerateEasing | |
) | |
) togetherWith fadeOut( | |
tween( | |
durationMillis = DURATION_EXIT_SHORT, | |
easing = EmphasizedAccelerateEasing | |
) | |
) using SizeTransform { _, _ -> | |
tween(durationMillis = DURATION, easing = EmphasizedEasing) | |
} | |
}, | |
targetState = show | |
) { | |
if (it) { | |
ConversationPageTransition( | |
data = conversation, | |
onBackPressed = { show = !show }) | |
} else { | |
Scaffold(containerColor = MaterialTheme.colorScheme.surfaceContainer) { values -> | |
Column(modifier = Modifier.padding(values)) { | |
CardDemoSearchBar( | |
modifier = Modifier | |
.align(Alignment.CenterHorizontally) | |
.padding(horizontal = 12.dp) | |
.padding(vertical = 16.dp) | |
) | |
LazyColumn( | |
verticalArrangement = Arrangement.spacedBy(8.dp), | |
contentPadding = PaddingValues(horizontal = 12.dp) | |
) { | |
items(ConversationList) { data -> | |
ConversationCardTransition( | |
data = data | |
) { | |
conversation = data | |
show = !show | |
} | |
} | |
} | |
} | |
} | |
} | |
} | |
} | |
} | |
} | |
context(SharedTransitionScope, AnimatedVisibilityScope) | |
@OptIn(ExperimentalAnimationSpecApi::class) | |
@Composable | |
fun ConversationCardTransition(data: Conversation, onClick: () -> Unit = {}) { | |
ConversationCard( | |
data = data, | |
containerModifier = Modifier.sharedBounds( | |
boundsTransform = { initial, target -> | |
tween(durationMillis = DURATION, easing = EmphasizedEasing) | |
}, | |
enter = fadeIn( | |
tween( | |
durationMillis = DURATION_ENTER, | |
delayMillis = DURATION_EXIT_SHORT, | |
easing = EmphasizedDecelerateEasing | |
) | |
), | |
exit = fadeOut( | |
tween( | |
durationMillis = DURATION_EXIT_SHORT, | |
easing = EmphasizedAccelerateEasing | |
) | |
), | |
sharedContentState = rememberSharedContentState(key = data), | |
animatedVisibilityScope = this@AnimatedVisibilityScope, | |
placeHolderSize = SharedTransitionScope.PlaceHolderSize.animatedSize, | |
), | |
imageModifier = Modifier.sharedElement( | |
// boundsTransform = arcBoundsTransform(durationMillis = DURATION), | |
boundsTransform = { initial, target -> | |
tween(durationMillis = DURATION, easing = EmphasizedEasing) | |
}, | |
state = rememberSharedContentState(key = data.avatarResId), | |
animatedVisibilityScope = this@AnimatedVisibilityScope, | |
placeHolderSize = SharedTransitionScope.PlaceHolderSize.contentSize, | |
), | |
onClick = onClick | |
) | |
} | |
private val RectToVector: TwoWayConverter<Rect, AnimationVector4D> = | |
TwoWayConverter( | |
convertToVector = { | |
Log.d("Hey", "hey!") | |
AnimationVector4D(it.center.x, it.center.y, it.height, it.width) | |
}, | |
convertFromVector = { | |
Rect(it.v1, it.v2, it.v3, it.v4) | |
} | |
) | |
context(SharedTransitionScope, AnimatedVisibilityScope) | |
@Composable | |
fun ConversationPageTransition(data: Conversation, onBackPressed: () -> Unit) { | |
ConversationPage( | |
data = data, containerModifier = Modifier.sharedBounds( | |
boundsTransform = { initial, target -> | |
tween(durationMillis = DURATION, easing = EmphasizedEasing) | |
}, | |
enter = fadeIn( | |
tween( | |
durationMillis = DURATION_ENTER, | |
delayMillis = DURATION_EXIT_SHORT, | |
easing = EmphasizedDecelerateEasing | |
) | |
), | |
exit = fadeOut( | |
tween( | |
durationMillis = DURATION_EXIT_SHORT, | |
easing = EmphasizedAccelerateEasing | |
) | |
), | |
sharedContentState = rememberSharedContentState(key = data), | |
animatedVisibilityScope = this@AnimatedVisibilityScope, | |
placeHolderSize = SharedTransitionScope.PlaceHolderSize.animatedSize, | |
), | |
imageModifier = Modifier.sharedElement( | |
boundsTransform = { initial, target -> | |
tween(durationMillis = DURATION, easing = EmphasizedEasing) | |
}, | |
state = rememberSharedContentState(key = data.avatarResId), | |
animatedVisibilityScope = this@AnimatedVisibilityScope, | |
placeHolderSize = SharedTransitionScope.PlaceHolderSize.animatedSize, | |
), | |
navigationIconModifier = Modifier | |
.renderInSharedTransitionScopeOverlay(zIndexInOverlay = -1f) | |
.animateEnterExit( | |
enter = scaleIn( | |
animationSpec = tween( | |
durationMillis = DURATION_ENTER, | |
delayMillis = DURATION_EXIT_SHORT, | |
easing = EmphasizedDecelerateEasing | |
) | |
) + fadeIn( | |
animationSpec = tween( | |
DURATION_ENTER, | |
delayMillis = DURATION_EXIT_SHORT, | |
easing = EmphasizedDecelerateEasing | |
) | |
), exit = scaleOut( | |
animationSpec = tween( | |
durationMillis = DURATION_EXIT_SHORT, | |
delayMillis = 0, | |
easing = EmphasizedAccelerateEasing | |
) | |
) + fadeOut( | |
animationSpec = tween( | |
durationMillis = DURATION_EXIT_SHORT, | |
delayMillis = 0, | |
easing = EmphasizedAccelerateEasing | |
) | |
) | |
), | |
onBackPressed = onBackPressed | |
) | |
} | |
@Composable | |
fun ConversationCard( | |
containerModifier: Modifier = Modifier, | |
imageModifier: Modifier = Modifier, | |
data: Conversation, | |
onClick: () -> Unit = {} | |
) { | |
Card( | |
modifier = containerModifier, | |
onClick = onClick, | |
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface) | |
) { | |
Box(modifier = Modifier) { | |
Row( | |
modifier = Modifier.padding(vertical = 16.dp), | |
verticalAlignment = Alignment.CenterVertically | |
) { | |
Spacer(modifier = Modifier.width(16.dp)) | |
Image( | |
painter = painterResource(id = data.avatarResId), | |
contentDescription = null, | |
modifier = imageModifier | |
.clip(CircleShape) | |
.size(60.dp), | |
contentScale = ContentScale.Crop | |
) | |
Spacer(modifier = Modifier.width(12.dp)) | |
Column() { | |
Row(verticalAlignment = Alignment.Bottom) { | |
Text( | |
text = data.name, | |
style = MaterialTheme.typography.bodyMedium, | |
maxLines = 1, | |
modifier = Modifier.weight(1f) | |
) | |
Text( | |
text = data.lastReplyTime, | |
style = MaterialTheme.typography.labelMedium, | |
maxLines = 1, | |
color = MaterialTheme.colorScheme.onSurfaceVariant | |
) | |
Spacer(modifier = Modifier.width(16.dp)) | |
} | |
Column(modifier = Modifier.padding(end = 48.dp)) { | |
Text( | |
text = data.title, | |
style = MaterialTheme.typography.bodySmall, | |
maxLines = 1 | |
) | |
Text( | |
text = data.content, | |
style = MaterialTheme.typography.bodySmall, | |
maxLines = 1, | |
color = MaterialTheme.colorScheme.onSurfaceVariant, | |
overflow = TextOverflow.Ellipsis | |
) | |
} | |
} | |
Spacer(modifier = Modifier.width(12.dp)) | |
} | |
IconToggleButton( | |
checked = data.isFavorite, | |
onCheckedChange = {}, modifier = Modifier | |
.align(Alignment.BottomEnd) | |
.padding(bottom = 4.dp), | |
colors = IconButtonDefaults.iconToggleButtonColors( | |
contentColor = MaterialTheme.colorScheme.outline, | |
checkedContentColor = MaterialTheme.colorScheme.primary | |
) | |
) { | |
Icon( | |
imageVector = if (data.isFavorite) Icons.Outlined.Star else Icons.Outlined.StarBorder, | |
contentDescription = null, | |
) | |
} | |
} | |
} | |
} | |
@Composable | |
@Preview | |
fun ConversationPage( | |
data: Conversation = ConversationList[1], | |
containerModifier: Modifier = Modifier, | |
imageModifier: Modifier = Modifier, | |
navigationIconModifier: Modifier = Modifier, | |
onBackPressed: () -> Unit = {} | |
) { | |
BackHandler { | |
onBackPressed() | |
} | |
Scaffold(containerColor = MaterialTheme.colorScheme.surfaceContainer, topBar = { | |
Row( | |
modifier = Modifier | |
.statusBarsPadding() | |
.padding(horizontal = 24.dp) | |
.padding(top = 24.dp) | |
.padding(bottom = 12.dp) | |
) { | |
FilledIconButton( | |
onClick = onBackPressed, | |
colors = IconButtonDefaults.filledIconButtonColors(containerColor = MaterialTheme.colorScheme.surfaceContainerLowest), | |
modifier = navigationIconModifier | |
) { | |
Icon( | |
imageVector = Icons.Outlined.ArrowBack, | |
contentDescription = null, | |
modifier = Modifier.size(20.dp) | |
) | |
} | |
Column( | |
modifier = Modifier.weight(1f), | |
horizontalAlignment = Alignment.CenterHorizontally, | |
verticalArrangement = Arrangement.Center | |
) { | |
Text(text = data.title, style = MaterialTheme.typography.bodyLarge, maxLines = 1) | |
Text( | |
text = "3 Messages", | |
style = MaterialTheme.typography.bodySmall, | |
color = MaterialTheme.colorScheme.onSurfaceVariant, | |
maxLines = 1 | |
) | |
} | |
IconButton( | |
onClick = { }, | |
colors = IconButtonDefaults.iconButtonColors(contentColor = MaterialTheme.colorScheme.onSurfaceVariant) | |
) { | |
Icon( | |
imageVector = Icons.Outlined.MoreVert, | |
contentDescription = null, | |
modifier = Modifier.size(20.dp) | |
) | |
} | |
} | |
}) { | |
Column( | |
modifier = Modifier | |
.padding(it) | |
.padding(horizontal = 12.dp) | |
.padding(top = 4.dp) | |
) { | |
Card( | |
modifier = containerModifier, | |
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainerLowest) | |
) { | |
Box(modifier = Modifier) { | |
Column( | |
modifier = Modifier | |
.padding(vertical = 24.dp) | |
.padding(start = 24.dp, end = 16.dp) | |
) { | |
Row( | |
modifier = Modifier.fillMaxWidth(), | |
verticalAlignment = Alignment.CenterVertically | |
) { | |
Image( | |
painter = painterResource(id = data.avatarResId), | |
contentDescription = null, | |
modifier = imageModifier | |
.clip(CircleShape) | |
.size(48.dp), | |
contentScale = ContentScale.Crop | |
) | |
Spacer(modifier = Modifier.width(12.dp)) | |
Column(modifier = Modifier.weight(1f)) { | |
Text( | |
text = data.name, | |
style = MaterialTheme.typography.bodyMedium, | |
maxLines = 1, | |
) | |
Text( | |
text = data.lastReplyTime, | |
style = MaterialTheme.typography.bodySmall, | |
maxLines = 1, | |
color = MaterialTheme.colorScheme.onSurfaceVariant | |
) | |
} | |
FilledIconToggleButton( | |
checked = data.isFavorite, | |
onCheckedChange = {}, modifier = Modifier, | |
colors = IconButtonDefaults.filledIconToggleButtonColors( | |
containerColor = MaterialTheme.colorScheme.surfaceContainer, | |
contentColor = MaterialTheme.colorScheme.outline, | |
checkedContentColor = MaterialTheme.colorScheme.primary, | |
checkedContainerColor = MaterialTheme.colorScheme.primaryContainer | |
) | |
) { | |
Icon( | |
imageVector = if (data.isFavorite) Icons.Outlined.Star else Icons.Outlined.StarBorder, | |
contentDescription = null, | |
) | |
} | |
} | |
Column(modifier = Modifier.padding(end = 8.dp)) { | |
Text( | |
text = "To Amy, Bob and Cat", | |
color = MaterialTheme.colorScheme.onSurfaceVariant, | |
modifier = Modifier.padding(vertical = 20.dp), | |
style = MaterialTheme.typography.bodySmall | |
) | |
Text( | |
text = data.content, | |
style = MaterialTheme.typography.bodyMedium, | |
minLines = 10, | |
color = MaterialTheme.colorScheme.onSurface, | |
overflow = TextOverflow.Ellipsis | |
) | |
} | |
} | |
} | |
Row( | |
modifier = Modifier | |
.padding(horizontal = 24.dp) | |
.padding(bottom = 16.dp) | |
) { | |
FilledTonalButton( | |
onClick = { /*TODO*/ }, | |
modifier = Modifier.weight(1f), | |
colors = ButtonDefaults.filledTonalButtonColors( | |
containerColor = MaterialTheme.colorScheme.surfaceContainerLow | |
) | |
) { | |
Text(text = "Reply") | |
} | |
Spacer(modifier = Modifier.width(12.dp)) | |
FilledTonalButton( | |
onClick = { /*TODO*/ }, | |
modifier = Modifier.weight(1f), | |
colors = ButtonDefaults.filledTonalButtonColors( | |
containerColor = MaterialTheme.colorScheme.surfaceContainerLow | |
) | |
) { | |
Text(text = "Reply all") | |
} | |
} | |
} | |
} | |
} | |
} | |
@Preview | |
@Composable | |
private fun ConversationPreview() { | |
Scaffold(containerColor = MaterialTheme.colorScheme.surfaceContainer) { | |
Column(modifier = Modifier.padding(it)) { | |
CardDemoSearchBar( | |
modifier = Modifier | |
.align(Alignment.CenterHorizontally) | |
.padding(horizontal = 8.dp) | |
.padding(vertical = 8.dp) | |
) | |
LazyColumn( | |
verticalArrangement = Arrangement.spacedBy(8.dp), | |
contentPadding = PaddingValues(horizontal = 8.dp) | |
) { | |
items(ConversationList) { | |
ConversationCard(data = it) | |
} | |
} | |
} | |
} | |
} | |
@Composable | |
@Preview | |
private fun CardDemoSearchBar(modifier: Modifier = Modifier) { | |
Surface( | |
shape = CircleShape, | |
color = MaterialTheme.colorScheme.surfaceContainerLowest, | |
modifier = modifier | |
.fillMaxWidth() | |
.height(56.dp) | |
) { | |
Row(verticalAlignment = Alignment.CenterVertically) { | |
Icon( | |
imageVector = Icons.Outlined.Search, | |
contentDescription = null, | |
tint = MaterialTheme.colorScheme.onSurfaceVariant, | |
modifier = Modifier.padding(horizontal = 16.dp) | |
) | |
Text( | |
text = "Search replies", | |
modifier = Modifier.weight(1f), | |
color = MaterialTheme.colorScheme.onSurfaceVariant, | |
style = MaterialTheme.typography.bodyMedium | |
) | |
Image( | |
painter = painterResource(id = R.drawable.img_2), | |
contentDescription = null, | |
contentScale = ContentScale.Crop, | |
modifier = Modifier | |
.padding(horizontal = 16.dp) | |
.size(30.dp) | |
.clip(CircleShape) | |
) | |
} | |
} | |
} | |
@Composable | |
fun CardList() { | |
Scaffold { paddingValues -> | |
Column(modifier = Modifier.padding(paddingValues)) { | |
CardDemoSearchBar( | |
modifier = Modifier | |
.align(Alignment.CenterHorizontally) | |
.padding(horizontal = 8.dp) | |
.padding(vertical = 16.dp) | |
) | |
} | |
} | |
} | |
data class Conversation( | |
val name: String, | |
val title: String, | |
val content: String, | |
val lastReplyTime: String, | |
@DrawableRes val avatarResId: Int, | |
val isFavorite: Boolean = false | |
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.example.compose_debug | |
import android.view.animation.PathInterpolator | |
import androidx.compose.animation.core.Easing | |
// Material 3 Emphasized Easing | |
// https://m3.material.io/styles/motion/easing-and-duration/tokens-specs | |
const val DURATION = 600 | |
const val DURATION_ENTER = 400 | |
const val DURATION_ENTER_SHORT = 300 | |
const val DURATION_EXIT = 200 | |
const val DURATION_EXIT_SHORT = 100 | |
private val emphasizedPath = android.graphics.Path().apply { | |
moveTo(0f, 0f) | |
cubicTo(0.05f, 0f, 0.133333f, 0.06f, 0.166666f, 0.4f) | |
cubicTo(0.208333f, 0.82f, 0.25f, 1f, 1f, 1f) | |
} | |
val emphasizedDecelerate = PathInterpolator(0.05f, 0.7f, 0.1f, 1f) | |
val emphasizedAccelerate = PathInterpolator(0.3f, 0f, 0.8f, 0.15f) | |
val emphasized = PathInterpolator(emphasizedPath) | |
val EmphasizedEasing: Easing = Easing { fraction -> emphasized.getInterpolation(fraction) } | |
val EmphasizedDecelerateEasing = | |
Easing { fraction -> emphasizedDecelerate.getInterpolation(fraction) } | |
val EmphasizedAccelerateEasing = | |
Easing { fraction -> emphasizedAccelerate.getInterpolation(fraction) } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment