mirror of
https://github.com/fankes/pagecurl-multiplatform.git
synced 2025-09-06 10:45:43 +08:00
Add state and snapTo method
This commit is contained in:
@@ -8,39 +8,37 @@ import androidx.activity.compose.setContent
|
|||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.Text
|
import androidx.compose.material.Text
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
import eu.wewox.pagecurl.components.SettingsPopup
|
||||||
import eu.wewox.pagecurl.config.PageCurlConfig
|
import eu.wewox.pagecurl.config.PageCurlConfig
|
||||||
import eu.wewox.pagecurl.page.PageCurl
|
import eu.wewox.pagecurl.page.PageCurl
|
||||||
|
import eu.wewox.pagecurl.page.rememberPageCurlState
|
||||||
import eu.wewox.pagecurl.ui.theme.PageCurlTheme
|
import eu.wewox.pagecurl.ui.theme.PageCurlTheme
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContent {
|
setContent {
|
||||||
PageCurlTheme {
|
PageCurlTheme {
|
||||||
var current by remember { mutableStateOf(0) }
|
Column {
|
||||||
val count = 6
|
val scope = rememberCoroutineScope()
|
||||||
|
val state = rememberPageCurlState(max = 6)
|
||||||
|
|
||||||
PageCurl(
|
PageCurl(
|
||||||
current = current,
|
state = state,
|
||||||
count = count,
|
|
||||||
onCurrentChange = {
|
|
||||||
current = it
|
|
||||||
},
|
|
||||||
config = PageCurlConfig(),
|
config = PageCurlConfig(),
|
||||||
) { index ->
|
) { index ->
|
||||||
Box(
|
Box(
|
||||||
@@ -56,14 +54,6 @@ class MainActivity : ComponentActivity() {
|
|||||||
contentScale = ContentScale.Crop,
|
contentScale = ContentScale.Crop,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
count - 1 -> {
|
|
||||||
Text(
|
|
||||||
text = "The End",
|
|
||||||
fontSize = 34.sp,
|
|
||||||
textAlign = TextAlign.Center,
|
|
||||||
modifier = Modifier.align(Alignment.Center)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
else -> {
|
else -> {
|
||||||
Text(
|
Text(
|
||||||
text = if (index % 2 == 1) Data.Lorem1 else Data.Lorem2,
|
text = if (index % 2 == 1) Data.Lorem1 else Data.Lorem2,
|
||||||
@@ -72,6 +62,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = index.toString(),
|
text = index.toString(),
|
||||||
color = Color.White,
|
color = Color.White,
|
||||||
@@ -80,6 +71,21 @@ class MainActivity : ComponentActivity() {
|
|||||||
.background(Color.Black, RoundedCornerShape(topStartPercent = 100))
|
.background(Color.Black, RoundedCornerShape(topStartPercent = 100))
|
||||||
.padding(16.dp)
|
.padding(16.dp)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
SettingsPopup(
|
||||||
|
onSnapToFirst = {
|
||||||
|
scope.launch {
|
||||||
|
state.snapTo(0)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSnapToLast = {
|
||||||
|
scope.launch {
|
||||||
|
state.snapTo(state.max - 1)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = Modifier.align(Alignment.Center)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,93 @@
|
|||||||
|
package eu.wewox.pagecurl.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.Card
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.material.TextButton
|
||||||
|
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.composed
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.window.Popup
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun SettingsPopup(
|
||||||
|
onSnapToFirst: () -> Unit,
|
||||||
|
onSnapToLast: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
var showPopup by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.size(100.dp)
|
||||||
|
.clickableWithoutRipple {
|
||||||
|
showPopup = true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (showPopup) {
|
||||||
|
Box(Modifier.fillMaxSize()) {
|
||||||
|
Popup(
|
||||||
|
alignment = Alignment.Center,
|
||||||
|
onDismissRequest = { showPopup = false }
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
shape = RoundedCornerShape(24.dp),
|
||||||
|
backgroundColor = MaterialTheme.colors.primary,
|
||||||
|
elevation = 16.dp
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
modifier = Modifier.padding(8.dp)
|
||||||
|
) {
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
onSnapToFirst()
|
||||||
|
showPopup = false
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Go to first",
|
||||||
|
color = MaterialTheme.colors.onPrimary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
onSnapToLast()
|
||||||
|
showPopup = false
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Go to last",
|
||||||
|
color = MaterialTheme.colors.onPrimary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Modifier.clickableWithoutRipple(onClick: () -> Unit) = composed {
|
||||||
|
clickable(
|
||||||
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
|
indication = null,
|
||||||
|
onClick = onClick
|
||||||
|
)
|
||||||
|
}
|
@@ -16,21 +16,18 @@ public data class PageCurlConfig(
|
|||||||
|
|
||||||
@ExperimentalPageCurlApi
|
@ExperimentalPageCurlApi
|
||||||
public data class InteractionConfig(
|
public data class InteractionConfig(
|
||||||
val forward: CurlDirection.Forward = CurlDirection.Forward(
|
val forward: CurlDirection = CurlDirection(
|
||||||
Rect(Offset(0.5f, 0.0f), Offset(1.0f, 1.0f)),
|
Rect(Offset(0.5f, 0.0f), Offset(1.0f, 1.0f)),
|
||||||
Rect(Offset(0.0f, 0.0f), Offset(0.5f, 1.0f)),
|
Rect(Offset(0.0f, 0.0f), Offset(0.5f, 1.0f)),
|
||||||
),
|
),
|
||||||
val backward: CurlDirection.Backward = CurlDirection.Backward(forward.end, forward.start),
|
val backward: CurlDirection = CurlDirection(forward.end, forward.start),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ExperimentalPageCurlApi
|
@ExperimentalPageCurlApi
|
||||||
public sealed interface CurlDirection {
|
public data class CurlDirection(
|
||||||
public val start: Rect
|
val start: Rect,
|
||||||
public val end: Rect
|
val end: Rect
|
||||||
|
)
|
||||||
public data class Forward(override val start: Rect, override val end: Rect) : CurlDirection
|
|
||||||
public data class Backward(override val start: Rect, override val end: Rect) : CurlDirection
|
|
||||||
}
|
|
||||||
|
|
||||||
@ExperimentalPageCurlApi
|
@ExperimentalPageCurlApi
|
||||||
public data class CurlConfig(
|
public data class CurlConfig(
|
||||||
|
@@ -19,13 +19,14 @@ import kotlin.math.PI
|
|||||||
|
|
||||||
@ExperimentalPageCurlApi
|
@ExperimentalPageCurlApi
|
||||||
internal fun Modifier.curlGesture(
|
internal fun Modifier.curlGesture(
|
||||||
|
key: Any?,
|
||||||
enabled: Boolean,
|
enabled: Boolean,
|
||||||
direction: CurlDirection,
|
direction: CurlDirection,
|
||||||
onStart: () -> Unit,
|
onStart: () -> Unit,
|
||||||
onCurl: (Offset, Offset) -> Unit,
|
onCurl: (Offset, Offset) -> Unit,
|
||||||
onEnd: () -> Unit,
|
onEnd: () -> Unit,
|
||||||
onCancel: () -> Unit,
|
onCancel: () -> Unit,
|
||||||
): Modifier = pointerInput(enabled) {
|
): Modifier = pointerInput(enabled, key) {
|
||||||
if (!enabled) {
|
if (!enabled) {
|
||||||
return@pointerInput
|
return@pointerInput
|
||||||
}
|
}
|
||||||
@@ -53,6 +54,11 @@ internal fun Modifier.curlGesture(
|
|||||||
onCurl(dragCurrent - vector, dragCurrent + vector)
|
onCurl(dragCurrent - vector, dragCurrent + vector)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (dragCurrent == dragStart) {
|
||||||
|
onCancel()
|
||||||
|
return@awaitPointerEventScope
|
||||||
|
}
|
||||||
|
|
||||||
val velocity = velocityTracker.calculateVelocity()
|
val velocity = velocityTracker.calculateVelocity()
|
||||||
val decay = splineBasedDecay<Offset>(this)
|
val decay = splineBasedDecay<Offset>(this)
|
||||||
val target = decay.calculateTargetValue(
|
val target = decay.calculateTargetValue(
|
||||||
|
@@ -8,11 +8,15 @@ import androidx.compose.foundation.layout.Box
|
|||||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.rememberUpdatedState
|
import androidx.compose.runtime.rememberUpdatedState
|
||||||
|
import androidx.compose.runtime.saveable.Saver
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.unit.Constraints
|
||||||
import eu.wewox.pagecurl.ExperimentalPageCurlApi
|
import eu.wewox.pagecurl.ExperimentalPageCurlApi
|
||||||
import eu.wewox.pagecurl.config.CurlDirection
|
import eu.wewox.pagecurl.config.CurlDirection
|
||||||
import eu.wewox.pagecurl.config.PageCurlConfig
|
import eu.wewox.pagecurl.config.PageCurlConfig
|
||||||
@@ -22,58 +26,54 @@ import kotlinx.coroutines.launch
|
|||||||
@ExperimentalPageCurlApi
|
@ExperimentalPageCurlApi
|
||||||
@Composable
|
@Composable
|
||||||
public fun PageCurl(
|
public fun PageCurl(
|
||||||
current: Int,
|
state: PageCurlState,
|
||||||
count: Int,
|
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onCurrentChange: (Int) -> Unit,
|
|
||||||
config: PageCurlConfig = PageCurlConfig(),
|
config: PageCurlConfig = PageCurlConfig(),
|
||||||
content: @Composable (Int) -> Unit
|
content: @Composable (Int) -> Unit
|
||||||
) {
|
) {
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val updatedCurrent by rememberUpdatedState(current)
|
val updatedCurrent by rememberUpdatedState(state.current)
|
||||||
|
|
||||||
BoxWithConstraints(modifier) {
|
BoxWithConstraints(modifier) {
|
||||||
val maxWidthPx = constraints.maxWidth.toFloat()
|
state.setup(constraints)
|
||||||
val maxHeightPx = constraints.maxHeight.toFloat()
|
|
||||||
val left = Curl(Offset(0f, 0f), Offset(0f, maxHeightPx))
|
|
||||||
val right = Curl(Offset(maxWidthPx, 0f), Offset(maxWidthPx, maxHeightPx))
|
|
||||||
|
|
||||||
val forward = remember { Animatable(right, Curl.VectorConverter, Curl.VisibilityThreshold) }
|
val internalState by rememberUpdatedState(state.internalState ?: return@BoxWithConstraints)
|
||||||
val backward = remember { Animatable(left, Curl.VectorConverter, Curl.VisibilityThreshold) }
|
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
Modifier
|
Modifier
|
||||||
.curlGesture(
|
.curlGesture(
|
||||||
enabled = updatedCurrent < count - 1,
|
key = internalState,
|
||||||
|
enabled = updatedCurrent < state.max - 1,
|
||||||
scope = scope,
|
scope = scope,
|
||||||
direction = config.interaction.forward,
|
direction = config.interaction.forward,
|
||||||
start = right,
|
start = internalState.rightCurl,
|
||||||
end = left,
|
end = internalState.leftCurl,
|
||||||
animatable = forward,
|
animatable = internalState.forward,
|
||||||
onChange = { onCurrentChange(updatedCurrent + 1) }
|
onChange = { state.current = updatedCurrent + 1 }
|
||||||
)
|
)
|
||||||
.curlGesture(
|
.curlGesture(
|
||||||
|
key = internalState,
|
||||||
enabled = updatedCurrent > 0,
|
enabled = updatedCurrent > 0,
|
||||||
scope = scope,
|
scope = scope,
|
||||||
direction = config.interaction.backward,
|
direction = config.interaction.backward,
|
||||||
start = left,
|
start = internalState.leftCurl,
|
||||||
end = right,
|
end = internalState.rightCurl,
|
||||||
animatable = backward,
|
animatable = internalState.backward,
|
||||||
onChange = { onCurrentChange(updatedCurrent - 1) }
|
onChange = { state.current = updatedCurrent - 1 }
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
if (updatedCurrent + 1 < count) {
|
if (updatedCurrent + 1 < state.max) {
|
||||||
content(updatedCurrent + 1)
|
content(updatedCurrent + 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (updatedCurrent < count) {
|
if (updatedCurrent < state.max) {
|
||||||
Box(Modifier.drawCurl(config.curl, forward.value.a, forward.value.b)) {
|
Box(Modifier.drawCurl(config.curl, internalState.forward.value.a, internalState.forward.value.b)) {
|
||||||
content(updatedCurrent)
|
content(updatedCurrent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (updatedCurrent > 0) {
|
if (updatedCurrent > 0) {
|
||||||
Box(Modifier.drawCurl(config.curl, backward.value.a, backward.value.b)) {
|
Box(Modifier.drawCurl(config.curl, internalState.backward.value.a, internalState.backward.value.b)) {
|
||||||
content(updatedCurrent - 1)
|
content(updatedCurrent - 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -81,8 +81,79 @@ public fun PageCurl(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ExperimentalPageCurlApi
|
||||||
|
@Composable
|
||||||
|
public fun rememberPageCurlState(
|
||||||
|
max: Int,
|
||||||
|
initialCurrent: Int = 0,
|
||||||
|
): PageCurlState =
|
||||||
|
rememberSaveable(
|
||||||
|
max, initialCurrent,
|
||||||
|
saver = Saver(
|
||||||
|
save = { it.current },
|
||||||
|
restore = {
|
||||||
|
PageCurlState(
|
||||||
|
max = it,
|
||||||
|
initialCurrent = initialCurrent
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
PageCurlState(
|
||||||
|
max = max,
|
||||||
|
initialCurrent = initialCurrent
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExperimentalPageCurlApi
|
||||||
|
public class PageCurlState(
|
||||||
|
public val max: Int,
|
||||||
|
initialCurrent: Int = 0,
|
||||||
|
) {
|
||||||
|
public var current: Int by mutableStateOf(initialCurrent)
|
||||||
|
internal set
|
||||||
|
|
||||||
|
internal var internalState: InternalState? by mutableStateOf(null)
|
||||||
|
|
||||||
|
internal fun setup(constraints: Constraints) {
|
||||||
|
if (internalState?.constraints == constraints) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val maxWidthPx = constraints.maxWidth.toFloat()
|
||||||
|
val maxHeightPx = constraints.maxHeight.toFloat()
|
||||||
|
|
||||||
|
val left = Curl(Offset(0f, 0f), Offset(0f, maxHeightPx))
|
||||||
|
val right = Curl(Offset(maxWidthPx, 0f), Offset(maxWidthPx, maxHeightPx))
|
||||||
|
|
||||||
|
val forward = Animatable(right, Curl.VectorConverter, Curl.VisibilityThreshold)
|
||||||
|
val backward = Animatable(left, Curl.VectorConverter, Curl.VisibilityThreshold)
|
||||||
|
|
||||||
|
internalState = InternalState(constraints, left, right, forward, backward)
|
||||||
|
}
|
||||||
|
|
||||||
|
public suspend fun snapTo(value: Int) {
|
||||||
|
internalState?.reset()
|
||||||
|
current = value
|
||||||
|
}
|
||||||
|
|
||||||
|
internal data class InternalState(
|
||||||
|
val constraints: Constraints,
|
||||||
|
val leftCurl: Curl,
|
||||||
|
val rightCurl: Curl,
|
||||||
|
val forward: Animatable<Curl, AnimationVector4D>,
|
||||||
|
val backward: Animatable<Curl, AnimationVector4D>,
|
||||||
|
) {
|
||||||
|
internal suspend fun reset() {
|
||||||
|
forward.snapTo(rightCurl)
|
||||||
|
backward.snapTo(leftCurl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ExperimentalPageCurlApi
|
@ExperimentalPageCurlApi
|
||||||
private fun Modifier.curlGesture(
|
private fun Modifier.curlGesture(
|
||||||
|
key: Any?,
|
||||||
enabled: Boolean,
|
enabled: Boolean,
|
||||||
scope: CoroutineScope,
|
scope: CoroutineScope,
|
||||||
direction: CurlDirection,
|
direction: CurlDirection,
|
||||||
@@ -92,6 +163,7 @@ private fun Modifier.curlGesture(
|
|||||||
onChange: () -> Unit,
|
onChange: () -> Unit,
|
||||||
): Modifier =
|
): Modifier =
|
||||||
curlGesture(
|
curlGesture(
|
||||||
|
key = key,
|
||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
direction = direction,
|
direction = direction,
|
||||||
onStart = {
|
onStart = {
|
||||||
@@ -121,8 +193,7 @@ private fun Modifier.curlGesture(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
internal data class Curl(val a: Offset, val b: Offset) {
|
||||||
private data class Curl(val a: Offset, val b: Offset) {
|
|
||||||
companion object {
|
companion object {
|
||||||
val VectorConverter: TwoWayConverter<Curl, AnimationVector4D> =
|
val VectorConverter: TwoWayConverter<Curl, AnimationVector4D> =
|
||||||
TwoWayConverter(
|
TwoWayConverter(
|
||||||
|
Reference in New Issue
Block a user