diff --git a/sample/src/main/java/com/patchself/compose/sample/hyperspace/HyperspaceAnimation.kt b/sample/src/main/java/com/patchself/compose/sample/hyperspace/HyperspaceAnimation.kt new file mode 100644 index 0000000..96e5eac --- /dev/null +++ b/sample/src/main/java/com/patchself/compose/sample/hyperspace/HyperspaceAnimation.kt @@ -0,0 +1,169 @@ +package com.patchself.compose.sample.hyperspace + +import androidx.compose.animation.core.* +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.StrokeCap +import kotlinx.coroutines.isActive +import kotlin.random.Random + +/** + * Hyperspace animation composable that renders a starfield effect + * + * @param state Current animation state (Cruise, Warp, or Deceleration) + * @param modifier Modifier for the canvas + * @param starCount Number of stars to render + * @param baseSpeed Base speed multiplier for star movement + * @param backgroundColor Background color + */ +@Composable +fun HyperspaceAnimation( + state: HyperspaceState, + modifier: Modifier = Modifier, + starCount: Int = 200, + baseSpeed: Float = 1f, + backgroundColor: Color = Color.Black +) { + // Animation progress for smooth transitions + val targetSpeed = when (state) { + HyperspaceState.Cruise -> 1f + HyperspaceState.Warp -> 15f + HyperspaceState.Deceleration -> 1f + } + + val animatedSpeed by animateFloatAsState( + targetValue = targetSpeed * baseSpeed, + animationSpec = tween( + durationMillis = when (state) { + HyperspaceState.Warp -> 1500 // Quick acceleration + HyperspaceState.Deceleration -> 3000 // Slow deceleration + else -> 1000 + }, + easing = when (state) { + HyperspaceState.Warp -> FastOutSlowInEasing + HyperspaceState.Deceleration -> LinearOutSlowInEasing + else -> LinearEasing + } + ), label = "speed" + ) + + // Star trail length based on speed + val targetTrailLength = when { + animatedSpeed > 10f -> 80f + animatedSpeed > 5f -> 40f + else -> 5f + } + + val animatedTrailLength by animateFloatAsState( + targetValue = targetTrailLength, + animationSpec = tween(1000), label = "trail" + ) + + // Initialize stars + val stars = remember { + List(starCount) { + Star( + x = 0f, + y = 0f, + z = Random.nextFloat() * 2f + 0.5f, + speed = Random.nextFloat() * 0.5f + 0.5f + ) + } + } + + // Continuous redraw for animation + LaunchedEffect(Unit) { + while (isActive) { + withFrameNanos { } + } + } + + Canvas(modifier = modifier.fillMaxSize()) { + val centerX = size.width / 2f + val centerY = size.height / 2f + val maxDistance = kotlin.math.sqrt(centerX * centerX + centerY * centerY) + + // Draw background + drawRect(backgroundColor) + + // Update and draw each star + stars.forEach { star -> + // Initialize star position if needed + if (star.x == 0f && star.y == 0f) { + star.reset(centerX, centerY) + } + + // Calculate direction from center + val dx = star.x - centerX + val dy = star.y - centerY + val distance = kotlin.math.sqrt(dx * dx + dy * dy) + + // Normalize direction (avoid division by zero) + val nx = if (distance > 0) dx / distance else 0f + val ny = if (distance > 0) dy / distance else 0f + + if (distance > 0) { + // Move star outward + val moveSpeed = animatedSpeed * star.speed * 0.5f + star.x += nx * moveSpeed + star.y += ny * moveSpeed + + // Update z position for depth effect + star.z = kotlin.math.max(0.1f, star.z - moveSpeed * 0.01f) + } + + // Get current position accounting for depth + val currentPos = star.getOffset(centerX, centerY) + + // Reset star if it goes off screen + if (currentPos.x < -50 || currentPos.x > size.width + 50 || + currentPos.y < -50 || currentPos.y > size.height + 50) { + star.reset(centerX, centerY) + } else { + // Calculate previous position for trail effect + val trailDistance = animatedTrailLength * star.speed + val prevX = star.x - nx * trailDistance + val prevY = star.y - ny * trailDistance + val prevZ = kotlin.math.min(3f, star.z + trailDistance * 0.01f) + + val prevScale = 1f / prevZ + val prevPos = Offset( + centerX + (prevX - centerX) * prevScale, + centerY + (prevY - centerY) * prevScale + ) + + val starSize = star.getSize() + val alpha = (1f - (distance / maxDistance)).coerceIn(0f, 1f) * star.color.alpha + + // Draw star as line (trail) or point based on speed + if (animatedTrailLength > 10f) { + // Draw as line (warp effect) + drawLine( + color = star.color.copy(alpha = alpha * 0.6f), + start = prevPos, + end = currentPos, + strokeWidth = starSize.coerceIn(0.5f, 3f), + cap = StrokeCap.Round + ) + // Draw brighter point at the end + drawCircle( + color = star.color.copy(alpha = alpha), + radius = starSize, + center = currentPos + ) + } else { + // Draw as point (cruise mode) + drawCircle( + color = star.color.copy(alpha = alpha), + radius = starSize, + center = currentPos + ) + } + } + } + } +} diff --git a/sample/src/main/java/com/patchself/compose/sample/hyperspace/HyperspacePage.kt b/sample/src/main/java/com/patchself/compose/sample/hyperspace/HyperspacePage.kt new file mode 100644 index 0000000..5041dce --- /dev/null +++ b/sample/src/main/java/com/patchself/compose/sample/hyperspace/HyperspacePage.kt @@ -0,0 +1,113 @@ +package com.patchself.compose.sample.hyperspace + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.material.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import com.patchself.compose.navigator.PageController +import com.patchself.compose.sample.R + +/** + * Demo page for the hyperspace animation + */ +class HyperspacePage : PageController() { + + override fun getId() = R.id.HyperspacePage + + @Composable + override fun ScreenContent() { + var currentState by remember { mutableStateOf(HyperspaceState.Cruise) } + + Box(modifier = Modifier.fillMaxSize()) { + // Hyperspace animation background + HyperspaceAnimation( + state = currentState, + modifier = Modifier.fillMaxSize() + ) + + // Top bar + TopAppBar( + title = { Text(text = "Hyperspace Animation") }, + navigationIcon = { + IconButton(onClick = { navigateBack() }) { + Icon( + Icons.Filled.ArrowBack, + contentDescription = "Back", + tint = Color.White + ) + } + }, + backgroundColor = Color.Black.copy(alpha = 0.5f), + contentColor = Color.White, + elevation = 0.dp + ) + + // Control buttons at bottom + Column( + modifier = Modifier + .align(Alignment.BottomCenter) + .fillMaxWidth() + .background(Color.Black.copy(alpha = 0.5f)) + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + Text( + text = when (currentState) { + HyperspaceState.Cruise -> "巡航状态 (Cruise Mode)" + HyperspaceState.Warp -> "超光速状态 (Warp Mode)" + HyperspaceState.Deceleration -> "减速中 (Decelerating)" + }, + color = Color.White, + style = MaterialTheme.typography.h6 + ) + + Row( + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + Button( + onClick = { currentState = HyperspaceState.Cruise }, + colors = ButtonDefaults.buttonColors( + backgroundColor = if (currentState == HyperspaceState.Cruise) + Color.Cyan else Color.DarkGray + ) + ) { + Text("巡航\nCruise") + } + + Button( + onClick = { currentState = HyperspaceState.Warp }, + colors = ButtonDefaults.buttonColors( + backgroundColor = if (currentState == HyperspaceState.Warp) + Color.Red else Color.DarkGray + ) + ) { + Text("超光速\nWarp") + } + + Button( + onClick = { currentState = HyperspaceState.Deceleration }, + colors = ButtonDefaults.buttonColors( + backgroundColor = if (currentState == HyperspaceState.Deceleration) + Color.Yellow else Color.DarkGray + ) + ) { + Text("减速\nSlow Down") + } + } + + Text( + text = "点击按钮切换动画状态", + color = Color.White.copy(alpha = 0.7f), + style = MaterialTheme.typography.caption + ) + } + } + } +} diff --git a/sample/src/main/java/com/patchself/compose/sample/hyperspace/HyperspaceState.kt b/sample/src/main/java/com/patchself/compose/sample/hyperspace/HyperspaceState.kt new file mode 100644 index 0000000..4d9fd15 --- /dev/null +++ b/sample/src/main/java/com/patchself/compose/sample/hyperspace/HyperspaceState.kt @@ -0,0 +1,21 @@ +package com.patchself.compose.sample.hyperspace + +/** + * Represents the different states of the hyperspace animation + */ +sealed class HyperspaceState { + /** + * Cruise mode - stars move slowly from center to edges as points + */ + object Cruise : HyperspaceState() + + /** + * Warp/Hyperspace mode - stars stretch into lines moving rapidly + */ + object Warp : HyperspaceState() + + /** + * Deceleration mode - transitioning from Warp back to Cruise + */ + object Deceleration : HyperspaceState() +} diff --git a/sample/src/main/java/com/patchself/compose/sample/hyperspace/Star.kt b/sample/src/main/java/com/patchself/compose/sample/hyperspace/Star.kt new file mode 100644 index 0000000..7084cfd --- /dev/null +++ b/sample/src/main/java/com/patchself/compose/sample/hyperspace/Star.kt @@ -0,0 +1,45 @@ +package com.patchself.compose.sample.hyperspace + +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import kotlin.random.Random + +/** + * Represents a single star in the hyperspace animation + */ +data class Star( + var x: Float, + var y: Float, + var z: Float, // Depth for 3D effect + var speed: Float, + val baseSize: Float = Random.nextFloat() * 2f + 1f, + val color: Color = Color.White.copy(alpha = Random.nextFloat() * 0.5f + 0.5f) +) { + /** + * Get the current position offset accounting for depth + */ + fun getOffset(centerX: Float, centerY: Float): Offset { + val scale = 1f / z + return Offset( + centerX + (x - centerX) * scale, + centerY + (y - centerY) * scale + ) + } + + /** + * Get the size of the star accounting for depth + */ + fun getSize(): Float { + return baseSize / z + } + + /** + * Reset star to center with new random properties + */ + fun reset(centerX: Float, centerY: Float) { + x = centerX + (Random.nextFloat() - 0.5f) * 100f + y = centerY + (Random.nextFloat() - 0.5f) * 100f + z = Random.nextFloat() * 2f + 0.5f + speed = Random.nextFloat() * 0.5f + 0.5f + } +} diff --git a/sample/src/main/java/com/patchself/compose/sample/ui/HomePage.kt b/sample/src/main/java/com/patchself/compose/sample/ui/HomePage.kt index 978314d..e921453 100644 --- a/sample/src/main/java/com/patchself/compose/sample/ui/HomePage.kt +++ b/sample/src/main/java/com/patchself/compose/sample/ui/HomePage.kt @@ -17,6 +17,7 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.patchself.compose.navigator.PageController import com.patchself.compose.sample.R +import com.patchself.compose.sample.hyperspace.HyperspacePage class HomePage : PageController() { @@ -56,6 +57,19 @@ class HomePage : PageController() { text = "Scroll Page", modifier = Modifier.align(Alignment.CenterHorizontally) ) + FloatingActionButton(onClick = { + navigateTo(HyperspacePage()) + }, Modifier.align(Alignment.CenterHorizontally)) { + Image(Icons.Filled.ArrowForward, + colorFilter = ColorFilter.tint(Color.White), + contentDescription = "" + ) + } + Spacer(modifier = Modifier.height(Dp(15f))) + Text( + text = "Hyperspace Animation", + modifier = Modifier.align(Alignment.CenterHorizontally) + ) if (fromThirdPage){ Spacer(modifier = Modifier.height(Dp(25f))) Text( diff --git a/sample/src/main/res/values/ids.xml b/sample/src/main/res/values/ids.xml index 6f32067..11fe7b1 100644 --- a/sample/src/main/res/values/ids.xml +++ b/sample/src/main/res/values/ids.xml @@ -4,4 +4,5 @@ + \ No newline at end of file