package com.suno.android.ui.screens.orpheus.components import androidx.compose.animation.core.rememberInfiniteTransition 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.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.widthIn import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf 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.res.stringResource import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.unit.dp import androidx.lifecycle.Lifecycle import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.repeatOnLifecycle import com.suno.android.R import com.suno.android.common_core_utils.environment.ThemeMode import com.suno.android.common_ui.theme.ExtendedTheme import com.suno.android.common_ui.theme.SunoTheme import kotlinx.coroutines.delay import com.suno.android.common_res.R as CommonR private const val TYPING_DURATION_MS = 800L private const val PAUSE_DURATION_MS = 3000L private const val DELETE_DELAY_PER_CHAR_MS = 15L private const val CYCLE_DELAY_MS = 500L @Composable fun WelcomeMessage( modifier: Modifier = Modifier, ) { val artworkResIds = remember { listOf( R.drawable.orpheus_floating_artwork_1, R.drawable.orpheus_floating_artwork_2, R.drawable.orpheus_floating_artwork_3, R.drawable.orpheus_floating_artwork_4, ) } val prompts = listOf( stringResource(CommonR.string.orpheus_welcome_prompt_1), stringResource(CommonR.string.orpheus_welcome_prompt_2), stringResource(CommonR.string.orpheus_welcome_prompt_3), stringResource(CommonR.string.orpheus_welcome_prompt_4), ) var currentPromptIndex by remember { mutableIntStateOf(0) } var displayedText by remember { mutableStateOf("") } val motionManager = rememberParallaxMotionManager() val lifecycleOwner = LocalLifecycleOwner.current LaunchedEffect(lifecycleOwner) { lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { while (true) { val currentText = prompts[currentPromptIndex] displayedText = "" val characters = currentText.toList() val delayPerChar = TYPING_DURATION_MS / characters.size val textBuilder = StringBuilder() characters.forEach { char -> textBuilder.append(char) displayedText = textBuilder.toString() delay(delayPerChar) } delay(PAUSE_DURATION_MS) for (i in displayedText.length downTo 0) { displayedText = displayedText.take(i) delay(DELETE_DELAY_PER_CHAR_MS) } delay(CYCLE_DELAY_MS) currentPromptIndex = (currentPromptIndex + 1) % prompts.size } } } SunoTheme( themeMode = ThemeMode.DARK, ) { Box( modifier = modifier .fillMaxSize() .padding(horizontal = 16.dp), contentAlignment = Alignment.Center, ) { FloatingArtworksBackground( artworkResIds = artworkResIds, gyroscopeOffsetX = motionManager.offsetX, gyroscopeOffsetY = motionManager.offsetY, ) Column( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(space = 16.dp), ) { Text( text = displayedText, style = ExtendedTheme.typography.extraLargeTitle, color = ExtendedTheme.colors.textPrimary, textAlign = TextAlign.Center, modifier = Modifier .widthIn(max = 250.dp) .semantics { contentDescription = displayedText }, ) } } } } @Composable private fun FloatingArtworksBackground( artworkResIds: List, gyroscopeOffsetX: Float, gyroscopeOffsetY: Float, ) { val infiniteTransition = rememberInfiniteTransition(label = "Floating artworks") // Large artwork - top left, rotated -30 degrees FloatingArtwork( imageResId = artworkResIds[1], modifier = Modifier .offset(x = (-200).dp, y = (-260).dp) .parallaxMotion( gyroscopeOffsetX = gyroscopeOffsetX, gyroscopeOffsetY = gyroscopeOffsetY, parallaxMultiplier = 1.5f, ) .floatingAnimation( infiniteTransition = infiniteTransition, duration = 4500, delay = 0, label = "Artwork 1", ) .artworkStyle( size = 195.dp, cornerRadius = 32.dp, rotation = -30f, blurRadius = 3.dp, opacity = 0.8f, ), ) // Medium artwork - top right FloatingArtwork( imageResId = artworkResIds[0], modifier = Modifier .offset(x = 100.dp, y = (-250).dp) .parallaxMotion( gyroscopeOffsetX = gyroscopeOffsetX, gyroscopeOffsetY = gyroscopeOffsetY, parallaxMultiplier = 1.2f, ) .floatingAnimation( infiniteTransition = infiniteTransition, duration = 3800, delay = 800, label = "Artwork 2", ) .artworkStyle( size = 115.dp, cornerRadius = 24.dp, rotation = 0f, blurRadius = 5.dp, opacity = 0.5f, ), ) // Small artwork - middle left FloatingArtwork( imageResId = artworkResIds[2], modifier = Modifier .offset(x = (-100).dp, y = 120.dp) .parallaxMotion( gyroscopeOffsetX = gyroscopeOffsetX, gyroscopeOffsetY = gyroscopeOffsetY, parallaxMultiplier = 1.0f, ) .floatingAnimation( infiniteTransition = infiniteTransition, duration = 5200, delay = 1500, label = "Artwork 3", ) .artworkStyle( size = 86.dp, cornerRadius = 24.dp, rotation = 0f, blurRadius = 6.dp, opacity = 0.5f, ), ) // Large artwork - bottom right, rotated 15 degrees FloatingArtwork( imageResId = artworkResIds[3], modifier = Modifier .offset(x = 200.dp, y = 50.dp) .parallaxMotion( gyroscopeOffsetX = gyroscopeOffsetX, gyroscopeOffsetY = gyroscopeOffsetY, parallaxMultiplier = 1.5f, ) .floatingAnimation( infiniteTransition = infiniteTransition, duration = 4200, delay = 500, label = "Artwork 4", ) .artworkStyle( size = 195.dp, cornerRadius = 32.dp, rotation = 15f, blurRadius = 1.dp, opacity = 0.6f, ), ) } @PreviewLightDark @Composable private fun WelcomeMessage_Preview() { SunoTheme { WelcomeMessage() } }