mirror of
https://github.com/fankes/pagecurl-multiplatform.git
synced 2025-09-06 02:35:25 +08:00
Extract to library
This commit is contained in:
1
pagecurl/.gitignore
vendored
Normal file
1
pagecurl/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/build
|
43
pagecurl/build.gradle
Normal file
43
pagecurl/build.gradle
Normal file
@@ -0,0 +1,43 @@
|
||||
plugins {
|
||||
id "com.android.library"
|
||||
id "org.jetbrains.kotlin.android"
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdk 32
|
||||
|
||||
defaultConfig {
|
||||
minSdk 24
|
||||
targetSdk 32
|
||||
|
||||
consumerProguardFiles "consumer-rules.pro"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
buildFeatures {
|
||||
compose true
|
||||
}
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion compose_version
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
freeCompilerArgs = freeCompilerArgs +
|
||||
"-Xexplicit-api=strict" +
|
||||
"-Xopt-in=kotlin.RequiresOptIn"
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("androidx.compose.ui:ui:$compose_version")
|
||||
implementation("androidx.compose.foundation:foundation:$compose_version")
|
||||
}
|
0
pagecurl/consumer-rules.pro
Normal file
0
pagecurl/consumer-rules.pro
Normal file
21
pagecurl/proguard-rules.pro
vendored
Normal file
21
pagecurl/proguard-rules.pro
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
2
pagecurl/src/main/AndroidManifest.xml
Normal file
2
pagecurl/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="eu.wewox.pagecurl" />
|
@@ -0,0 +1,9 @@
|
||||
package eu.wewox.pagecurl
|
||||
|
||||
/**
|
||||
* Used for annotating experimental page curl API that is likely to change or be removed in the future.
|
||||
*/
|
||||
@RequiresOptIn(
|
||||
"This API is experimental and is likely to change or to be removed in the future."
|
||||
)
|
||||
public annotation class ExperimentalPageCurlApi
|
@@ -0,0 +1,53 @@
|
||||
package eu.wewox.pagecurl.config
|
||||
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.DpOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.wewox.pagecurl.ExperimentalPageCurlApi
|
||||
|
||||
@ExperimentalPageCurlApi
|
||||
public data class PageCurlConfig(
|
||||
val curl: CurlConfig = CurlConfig(),
|
||||
val interaction: InteractionConfig = InteractionConfig()
|
||||
)
|
||||
|
||||
@ExperimentalPageCurlApi
|
||||
public data class InteractionConfig(
|
||||
val forward: CurlDirection.Forward = CurlDirection.Forward(
|
||||
Rect(Offset(0.5f, 0.0f), Offset(1.0f, 1.0f)),
|
||||
Rect(Offset(0.0f, 0.0f), Offset(0.5f, 1.0f)),
|
||||
),
|
||||
val backward: CurlDirection.Backward = CurlDirection.Backward(forward.end, forward.start),
|
||||
)
|
||||
|
||||
@ExperimentalPageCurlApi
|
||||
public sealed interface CurlDirection {
|
||||
public val start: Rect
|
||||
public 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
|
||||
public data class CurlConfig(
|
||||
val backPage: BackPageConfig = BackPageConfig(),
|
||||
val shadow: ShadowConfig = ShadowConfig(),
|
||||
)
|
||||
|
||||
@ExperimentalPageCurlApi
|
||||
public data class BackPageConfig(
|
||||
val color: Color = Color.White,
|
||||
val contentAlpha: Float = 0.1f,
|
||||
)
|
||||
|
||||
@ExperimentalPageCurlApi
|
||||
public data class ShadowConfig(
|
||||
val color: Color = Color.Black,
|
||||
val alpha: Float = 0.2f,
|
||||
val radius: Dp = 15.dp,
|
||||
val offset: DpOffset = DpOffset((-5).dp, 0.dp),
|
||||
)
|
216
pagecurl/src/main/kotlin/eu/wewox/pagecurl/page/CurlDraw.kt
Normal file
216
pagecurl/src/main/kotlin/eu/wewox/pagecurl/page/CurlDraw.kt
Normal file
@@ -0,0 +1,216 @@
|
||||
package eu.wewox.pagecurl.page
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.os.Build
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.CacheDrawScope
|
||||
import androidx.compose.ui.draw.drawWithCache
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.toRect
|
||||
import androidx.compose.ui.graphics.Paint
|
||||
import androidx.compose.ui.graphics.Path
|
||||
import androidx.compose.ui.graphics.asAndroidPath
|
||||
import androidx.compose.ui.graphics.drawscope.ContentDrawScope
|
||||
import androidx.compose.ui.graphics.drawscope.clipPath
|
||||
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
|
||||
import androidx.compose.ui.graphics.drawscope.rotateRad
|
||||
import androidx.compose.ui.graphics.drawscope.withTransform
|
||||
import androidx.compose.ui.graphics.nativeCanvas
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.wewox.pagecurl.ExperimentalPageCurlApi
|
||||
import eu.wewox.pagecurl.config.CurlConfig
|
||||
import eu.wewox.pagecurl.utils.Polygon
|
||||
import eu.wewox.pagecurl.utils.lineLineIntersection
|
||||
import eu.wewox.pagecurl.utils.rotate
|
||||
import java.lang.Float.max
|
||||
import kotlin.math.atan2
|
||||
|
||||
@ExperimentalPageCurlApi
|
||||
public fun Modifier.drawCurl(
|
||||
config: CurlConfig = CurlConfig(),
|
||||
posA: Offset,
|
||||
posB: Offset,
|
||||
): Modifier = drawWithCache {
|
||||
fun drawOnlyContent() =
|
||||
onDrawWithContent {
|
||||
drawContent()
|
||||
}
|
||||
|
||||
if (posA == size.toRect().topLeft && posB == size.toRect().bottomLeft) {
|
||||
return@drawWithCache onDrawWithContent { }
|
||||
}
|
||||
|
||||
if (posA == size.toRect().topRight && posB == size.toRect().bottomRight) {
|
||||
return@drawWithCache drawOnlyContent()
|
||||
}
|
||||
|
||||
val topIntersection = lineLineIntersection(
|
||||
Offset(0f, 0f), Offset(size.width, 0f),
|
||||
posA, posB
|
||||
)
|
||||
val bottomIntersection = lineLineIntersection(
|
||||
Offset(0f, size.height), Offset(size.width, size.height),
|
||||
posA, posB
|
||||
)
|
||||
if (topIntersection == null || bottomIntersection == null) {
|
||||
return@drawWithCache drawOnlyContent()
|
||||
}
|
||||
|
||||
val topCurlOffset = Offset(max(0f, topIntersection.x), topIntersection.y)
|
||||
val bottomCurlOffset = Offset(max(0f, bottomIntersection.x), bottomIntersection.y)
|
||||
|
||||
val drawClippedContent = prepareClippedContent(topCurlOffset, bottomCurlOffset)
|
||||
val drawCurl = prepareCurl(config, topCurlOffset, bottomCurlOffset)
|
||||
|
||||
onDrawWithContent {
|
||||
drawClippedContent()
|
||||
drawCurl()
|
||||
}
|
||||
}
|
||||
|
||||
@ExperimentalPageCurlApi
|
||||
private fun CacheDrawScope.prepareClippedContent(
|
||||
topCurlOffset: Offset,
|
||||
bottomCurlOffset: Offset,
|
||||
): ContentDrawScope.() -> Unit {
|
||||
val path = Path()
|
||||
path.lineTo(topCurlOffset.x, topCurlOffset.y)
|
||||
path.lineTo(bottomCurlOffset.x, bottomCurlOffset.y)
|
||||
path.lineTo(0f, size.height)
|
||||
return result@{
|
||||
clipPath(path) {
|
||||
this@result.drawContent()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ExperimentalPageCurlApi
|
||||
private fun CacheDrawScope.prepareCurl(
|
||||
config: CurlConfig,
|
||||
topCurlOffset: Offset,
|
||||
bottomCurlOffset: Offset,
|
||||
): ContentDrawScope.() -> Unit {
|
||||
val polygon = Polygon(
|
||||
sequence {
|
||||
suspend fun SequenceScope<Offset>.yieldEndSideInterception() {
|
||||
val offset = lineLineIntersection(
|
||||
topCurlOffset, bottomCurlOffset,
|
||||
Offset(size.width, 0f), Offset(size.width, size.height)
|
||||
) ?: return
|
||||
yield(offset)
|
||||
yield(offset)
|
||||
}
|
||||
if (topCurlOffset.x < size.width) {
|
||||
yield(topCurlOffset)
|
||||
yield(Offset(size.width, topCurlOffset.y))
|
||||
} else {
|
||||
yieldEndSideInterception()
|
||||
}
|
||||
if (bottomCurlOffset.x < size.width) {
|
||||
yield(Offset(size.width, size.height))
|
||||
yield(bottomCurlOffset)
|
||||
} else {
|
||||
yieldEndSideInterception()
|
||||
}
|
||||
}.toList()
|
||||
)
|
||||
|
||||
val lineVector = topCurlOffset - bottomCurlOffset
|
||||
val angle = Math.PI.toFloat() + atan2(-lineVector.y, lineVector.x) * 2
|
||||
val drawShadow = prepareShadow(config, polygon, angle)
|
||||
|
||||
return result@{
|
||||
withTransform({
|
||||
scale(-1f, 1f, pivot = bottomCurlOffset)
|
||||
rotateRad(angle, pivot = bottomCurlOffset)
|
||||
}) {
|
||||
this@result.drawShadow()
|
||||
|
||||
clipPath(polygon.toPath()) {
|
||||
this@result.drawContent()
|
||||
|
||||
val overlayAlpha = 1f - config.backPage.contentAlpha
|
||||
drawRect(config.backPage.color.copy(alpha = overlayAlpha))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ExperimentalPageCurlApi
|
||||
private fun CacheDrawScope.prepareShadow(
|
||||
config: CurlConfig,
|
||||
polygon: Polygon,
|
||||
angle: Float
|
||||
): ContentDrawScope.() -> Unit {
|
||||
val shadow = config.shadow
|
||||
|
||||
if (shadow.alpha == 0f || shadow.radius == 0.dp) {
|
||||
return { /* No shadow is requested */ }
|
||||
}
|
||||
|
||||
val radius = shadow.radius.toPx()
|
||||
val shadowColor = shadow.color.copy(alpha = shadow.alpha).toArgb()
|
||||
val transparent = shadow.color.copy(alpha = 0f).toArgb()
|
||||
val shadowOffset = Offset(-shadow.offset.x.toPx(), shadow.offset.y.toPx())
|
||||
.rotate(2 * Math.PI.toFloat() - angle)
|
||||
val paint = Paint().apply {
|
||||
val frameworkPaint = asFrameworkPaint()
|
||||
frameworkPaint.color = transparent
|
||||
frameworkPaint.setShadowLayer(
|
||||
shadow.radius.toPx(),
|
||||
shadowOffset.x,
|
||||
shadowOffset.y,
|
||||
shadowColor
|
||||
)
|
||||
}
|
||||
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
prepareShadowApi28(radius, paint, polygon)
|
||||
} else {
|
||||
prepareShadowImage(radius, paint, polygon)
|
||||
}
|
||||
}
|
||||
|
||||
private fun prepareShadowApi28(
|
||||
radius: Float,
|
||||
paint: Paint,
|
||||
polygon: Polygon,
|
||||
): ContentDrawScope.() -> Unit = {
|
||||
drawIntoCanvas {
|
||||
it.nativeCanvas.drawPath(
|
||||
polygon
|
||||
.offset(radius).toPath()
|
||||
.asAndroidPath(),
|
||||
paint.asFrameworkPaint()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun CacheDrawScope.prepareShadowImage(
|
||||
radius: Float,
|
||||
paint: Paint,
|
||||
polygon: Polygon,
|
||||
): ContentDrawScope.() -> Unit {
|
||||
val bitmap = Bitmap.createBitmap(
|
||||
(size.width + radius * 4).toInt(),
|
||||
(size.height + radius * 4).toInt(),
|
||||
Bitmap.Config.ARGB_8888
|
||||
)
|
||||
Canvas(bitmap).apply {
|
||||
drawPath(
|
||||
polygon
|
||||
.translate(Offset(2 * radius, 2 * radius))
|
||||
.offset(radius).toPath()
|
||||
.asAndroidPath(),
|
||||
paint.asFrameworkPaint()
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
drawIntoCanvas {
|
||||
it.nativeCanvas.drawBitmap(bitmap, -2 * radius, -2 * radius, null)
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,82 @@
|
||||
package eu.wewox.pagecurl.page
|
||||
|
||||
import androidx.compose.animation.core.VectorConverter
|
||||
import androidx.compose.animation.core.calculateTargetValue
|
||||
import androidx.compose.animation.splineBasedDecay
|
||||
import androidx.compose.foundation.gestures.awaitFirstDown
|
||||
import androidx.compose.foundation.gestures.drag
|
||||
import androidx.compose.foundation.gestures.forEachGesture
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.input.pointer.util.VelocityTracker
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import eu.wewox.pagecurl.ExperimentalPageCurlApi
|
||||
import eu.wewox.pagecurl.config.CurlDirection
|
||||
import eu.wewox.pagecurl.utils.rotate
|
||||
import kotlin.math.PI
|
||||
|
||||
@ExperimentalPageCurlApi
|
||||
public fun Modifier.curlGesture(
|
||||
enabled: Boolean,
|
||||
direction: CurlDirection,
|
||||
onStart: () -> Unit,
|
||||
onCurl: (Offset, Offset) -> Unit,
|
||||
onEnd: () -> Unit,
|
||||
onCancel: () -> Unit,
|
||||
): Modifier = pointerInput(enabled) {
|
||||
if (!enabled) {
|
||||
return@pointerInput
|
||||
}
|
||||
|
||||
val velocityTracker = VelocityTracker()
|
||||
val startRect by lazy { direction.start.multiply(size) }
|
||||
val endRect by lazy { direction.end.multiply(size) }
|
||||
forEachGesture {
|
||||
awaitPointerEventScope {
|
||||
val down = awaitFirstDown(requireUnconsumed = false)
|
||||
if (!startRect.contains(down.position)) {
|
||||
return@awaitPointerEventScope
|
||||
}
|
||||
|
||||
val dragStart = down.position.copy(x = size.width.toFloat())
|
||||
|
||||
onStart()
|
||||
|
||||
var dragCurrent = dragStart
|
||||
drag(down.id) { change ->
|
||||
dragCurrent = change.position
|
||||
velocityTracker.addPosition(System.currentTimeMillis(), dragCurrent)
|
||||
change.consume()
|
||||
val vector = (dragStart - dragCurrent).rotate(PI.toFloat() / 2)
|
||||
onCurl(dragCurrent - vector, dragCurrent + vector)
|
||||
}
|
||||
|
||||
val velocity = velocityTracker.calculateVelocity()
|
||||
val decay = splineBasedDecay<Offset>(this)
|
||||
val target = decay.calculateTargetValue(
|
||||
Offset.VectorConverter,
|
||||
dragCurrent,
|
||||
Offset(velocity.x, velocity.y)
|
||||
).let {
|
||||
Offset(
|
||||
it.x.coerceIn(0f, size.width.toFloat() - 1),
|
||||
it.y.coerceIn(0f, size.height.toFloat() - 1)
|
||||
)
|
||||
}
|
||||
|
||||
if (endRect.contains(target)) {
|
||||
onEnd()
|
||||
} else {
|
||||
onCancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Rect.multiply(size: IntSize): Rect =
|
||||
Rect(
|
||||
topLeft = Offset(size.width * left, size.height * top),
|
||||
bottomRight = Offset(size.width * right, size.height * bottom),
|
||||
)
|
136
pagecurl/src/main/kotlin/eu/wewox/pagecurl/page/PageCurl.kt
Normal file
136
pagecurl/src/main/kotlin/eu/wewox/pagecurl/page/PageCurl.kt
Normal file
@@ -0,0 +1,136 @@
|
||||
package eu.wewox.pagecurl.page
|
||||
|
||||
import androidx.compose.animation.core.Animatable
|
||||
import androidx.compose.animation.core.AnimationVector4D
|
||||
import androidx.compose.animation.core.TwoWayConverter
|
||||
import androidx.compose.animation.core.VisibilityThreshold
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import eu.wewox.pagecurl.ExperimentalPageCurlApi
|
||||
import eu.wewox.pagecurl.config.CurlDirection
|
||||
import eu.wewox.pagecurl.config.PageCurlConfig
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@ExperimentalPageCurlApi
|
||||
@Composable
|
||||
public fun PageCurl(
|
||||
current: Int,
|
||||
count: Int,
|
||||
modifier: Modifier = Modifier,
|
||||
onCurrentChange: (Int) -> Unit,
|
||||
config: PageCurlConfig = PageCurlConfig(),
|
||||
content: @Composable (Int) -> Unit
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val updatedCurrent by rememberUpdatedState(current)
|
||||
|
||||
BoxWithConstraints(modifier) {
|
||||
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 = remember { Animatable(right, Curl.VectorConverter, Curl.VisibilityThreshold) }
|
||||
val backward = remember { Animatable(left, Curl.VectorConverter, Curl.VisibilityThreshold) }
|
||||
|
||||
Box(
|
||||
Modifier
|
||||
.curlGesture(
|
||||
enabled = updatedCurrent < count - 1,
|
||||
scope = scope,
|
||||
direction = config.interaction.forward,
|
||||
start = right,
|
||||
end = left,
|
||||
animatable = forward,
|
||||
onChange = { onCurrentChange(updatedCurrent + 1) }
|
||||
)
|
||||
.curlGesture(
|
||||
enabled = updatedCurrent > 0,
|
||||
scope = scope,
|
||||
direction = config.interaction.backward,
|
||||
start = left,
|
||||
end = right,
|
||||
animatable = backward,
|
||||
onChange = { onCurrentChange(updatedCurrent - 1) }
|
||||
)
|
||||
) {
|
||||
if (updatedCurrent + 1 < count) {
|
||||
content(updatedCurrent + 1)
|
||||
}
|
||||
|
||||
if (updatedCurrent < count) {
|
||||
Box(Modifier.drawCurl(config.curl, forward.value.a, forward.value.b)) {
|
||||
content(updatedCurrent)
|
||||
}
|
||||
}
|
||||
|
||||
if (updatedCurrent > 0) {
|
||||
Box(Modifier.drawCurl(config.curl, backward.value.a, backward.value.b)) {
|
||||
content(updatedCurrent - 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ExperimentalPageCurlApi
|
||||
private fun Modifier.curlGesture(
|
||||
enabled: Boolean,
|
||||
scope: CoroutineScope,
|
||||
direction: CurlDirection,
|
||||
start: Curl,
|
||||
end: Curl,
|
||||
animatable: Animatable<Curl, AnimationVector4D>,
|
||||
onChange: () -> Unit,
|
||||
): Modifier =
|
||||
curlGesture(
|
||||
enabled = enabled,
|
||||
direction = direction,
|
||||
onStart = {
|
||||
scope.launch {
|
||||
animatable.snapTo(start)
|
||||
}
|
||||
},
|
||||
onCurl = { a, b ->
|
||||
scope.launch {
|
||||
animatable.animateTo(Curl(a, b))
|
||||
}
|
||||
},
|
||||
onEnd = {
|
||||
scope.launch {
|
||||
try {
|
||||
animatable.animateTo(end)
|
||||
} finally {
|
||||
onChange()
|
||||
animatable.snapTo(start)
|
||||
}
|
||||
}
|
||||
},
|
||||
onCancel = {
|
||||
scope.launch {
|
||||
animatable.animateTo(start)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
private data class Curl(val a: Offset, val b: Offset) {
|
||||
companion object {
|
||||
val VectorConverter: TwoWayConverter<Curl, AnimationVector4D> =
|
||||
TwoWayConverter(
|
||||
convertToVector = { AnimationVector4D(it.a.x, it.a.y, it.b.x, it.b.y) },
|
||||
convertFromVector = { Curl(Offset(it.v1, it.v2), Offset(it.v3, it.v4)) }
|
||||
)
|
||||
|
||||
val VisibilityThreshold: Curl =
|
||||
Curl(Offset.VisibilityThreshold, Offset.VisibilityThreshold)
|
||||
}
|
||||
}
|
@@ -0,0 +1,71 @@
|
||||
package eu.wewox.pagecurl.utils
|
||||
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Path
|
||||
import kotlin.math.cos
|
||||
import kotlin.math.sin
|
||||
|
||||
internal data class Polygon(val vertices: List<Offset>) {
|
||||
|
||||
private val size: Int = vertices.size
|
||||
|
||||
fun translate(offset: Offset): Polygon =
|
||||
Polygon(vertices.map { it + offset })
|
||||
|
||||
fun offset(value: Float): Polygon {
|
||||
val edgeNormals = List(size) {
|
||||
val edge = vertices[index(it + 1)] - vertices[index(it)]
|
||||
Offset(edge.y, -edge.x).normalized()
|
||||
}
|
||||
|
||||
val vertexNormals = List(size) {
|
||||
(edgeNormals[index(it - 1)] + edgeNormals[index(it)]).normalized()
|
||||
}
|
||||
|
||||
return Polygon(
|
||||
vertices.mapIndexed { index, vertex ->
|
||||
vertex + vertexNormals[index] * value
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun toPath(): Path =
|
||||
Path().apply {
|
||||
vertices.forEachIndexed { index, vertex ->
|
||||
if (index == 0) {
|
||||
moveTo(vertex.x, vertex.y)
|
||||
} else {
|
||||
lineTo(vertex.x, vertex.y)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun index(i: Int) = ((i % size) + size) % size
|
||||
}
|
||||
|
||||
private fun Offset.normalized(): Offset {
|
||||
val distance = getDistance()
|
||||
return if (distance != 0f) this / distance else this
|
||||
}
|
||||
|
||||
internal fun Offset.rotate(angle: Float): Offset {
|
||||
val sin = sin(angle)
|
||||
val cos = cos(angle)
|
||||
return Offset(x * cos - y * sin, x * sin + y * cos)
|
||||
}
|
||||
|
||||
internal fun lineLineIntersection(
|
||||
line1a: Offset,
|
||||
line1b: Offset,
|
||||
line2a: Offset,
|
||||
line2b: Offset,
|
||||
): Offset? {
|
||||
val denominator = (line1a.x - line1b.x) * (line2a.y - line2b.y) - (line1a.y - line1b.y) * (line2a.x - line2b.x)
|
||||
if (denominator == 0f) return null
|
||||
|
||||
val x = ((line1a.x * line1b.y - line1a.y * line1b.x) * (line2a.x - line2b.x) -
|
||||
(line1a.x - line1b.x) * (line2a.x * line2b.y - line2a.y * line2b.x)) / denominator
|
||||
val y = ((line1a.x * line1b.y - line1a.y * line1b.x) * (line2a.y - line2b.y) -
|
||||
(line1a.y - line1b.y) * (line2a.x * line2b.y - line2a.y * line2b.x)) / denominator
|
||||
return Offset(x, y)
|
||||
}
|
Reference in New Issue
Block a user