adjust animation

This commit is contained in:
callebtc
2025-08-31 12:46:01 +02:00
parent ec1f545469
commit c929397351
@@ -1,85 +1,16 @@
package com.bitchat.android.ui package com.bitchat.android.ui
import androidx.compose.animation.core.*
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.*
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.sp
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlin.random.Random import kotlin.random.Random
import com.bitchat.android.ui.theme.BASE_FONT_SIZE
/**
* Matrix-style encryption animation for messages during PoW computation
* Animates message content with random characters that gradually resolve to real text
*/
@Composable
fun AnimatedMatrixText(
targetText: String,
isAnimating: Boolean,
modifier: Modifier = Modifier,
color: Color = Color.Unspecified,
fontSize: TextUnit = TextUnit.Unspecified,
fontWeight: FontWeight? = null
) {
// Continuous animation: loop random characters until isAnimating becomes false
var animatedText by remember(targetText) { mutableStateOf(targetText) }
LaunchedEffect(isAnimating, targetText) {
if (isAnimating && targetText.isNotEmpty()) {
val encryptedChars = "!@#$%^&*()_+-=[]{}|;:,.<>?~`".toCharArray()
while (true) {
val frame = CharArray(targetText.length) {
encryptedChars[Random.nextInt(encryptedChars.size)]
}
animatedText = String(frame)
delay(80L)
}
} else {
animatedText = targetText
}
}
val styledText = buildAnnotatedString {
animatedText.forEach { char ->
withStyle(
style = SpanStyle(
color = color,
fontFamily = FontFamily.Monospace,
fontWeight = fontWeight
)
) {
append(char)
}
}
}
Text(
text = styledText,
modifier = modifier,
fontSize = fontSize,
fontFamily = FontFamily.Monospace
)
}
/** /**
* Animation state for individual characters * Animation state for individual characters
@@ -90,63 +21,6 @@ private enum class CharacterAnimationState {
FINAL // Showing final decrypted character FINAL // Showing final decrypted character
} }
/**
* Animate a single character from encrypted to final state
*/
private suspend fun animateCharacter(
index: Int,
targetChar: Char,
currentText: () -> String,
updateText: (String) -> Unit,
updateState: (List<CharacterAnimationState>) -> Unit
) {
val encryptedChars = "!@#$%^&*()_+-=[]{}|;:,.<>?~`".toCharArray()
val animationDuration = Random.nextLong(1000, 3000) // 1-3 seconds of encryption
val startTime = System.currentTimeMillis()
// Phase 1: Animate with random encrypted characters
while (System.currentTimeMillis() - startTime < animationDuration) {
val randomChar = encryptedChars[Random.nextInt(encryptedChars.size)]
// Update the character at this index
val text = currentText()
if (index < text.length) {
val newText = text.toCharArray()
newText[index] = randomChar
updateText(String(newText))
}
delay(100) // Change character every 100ms
}
// Phase 2: Brief transition state (yellow)
val states = currentText().indices.map { i ->
when {
i == index -> CharacterAnimationState.DECRYPTING
i < index -> CharacterAnimationState.FINAL // Already revealed
else -> CharacterAnimationState.ENCRYPTED // Still encrypted
}
}
updateState(states)
delay(200) // Brief yellow flash
// Phase 3: Reveal final character
val text = currentText()
if (index < text.length) {
val newText = text.toCharArray()
newText[index] = targetChar
updateText(String(newText))
val finalStates = currentText().indices.map { i ->
when {
i <= index -> CharacterAnimationState.FINAL
else -> CharacterAnimationState.ENCRYPTED
}
}
updateState(finalStates)
}
}
/** /**
* Check if a message should be animated based on its mining state * Check if a message should be animated based on its mining state
*/ */
@@ -256,26 +130,69 @@ private fun AnimatedMessageDisplay(
var animatedContent by remember(message.content) { mutableStateOf(message.content) } var animatedContent by remember(message.content) { mutableStateOf(message.content) }
val isAnimating = shouldAnimateMessage(message.id) val isAnimating = shouldAnimateMessage(message.id)
// Character-by-character animation state like the JavaScript version
var characterStates by remember(message.content) {
mutableStateOf(message.content.map { char ->
if (char == ' ') CharacterAnimationState.FINAL else CharacterAnimationState.ENCRYPTED
})
}
// Update animated content when animation state changes // Update animated content when animation state changes
LaunchedEffect(isAnimating, message.content) { LaunchedEffect(isAnimating, message.content) {
if (isAnimating && message.content.isNotEmpty()) { if (isAnimating && message.content.isNotEmpty()) {
val encryptedChars = "!@#$%^&*()_+-=[]{}|;:,.<>?~`".toCharArray() val encryptedChars = "!@$%^&*()_+-=[]{}|;:,<>?".toCharArray()
val baseLength = message.content.length
// Loop animation continuously until PoW stops (LaunchedEffect will cancel on recomposition) // Start character animations with staggered delays (like JS version)
while (true) { message.content.forEachIndexed { index, targetChar ->
// Generate a new random frame of the same length as the real content if (targetChar != ' ') { // Skip spaces
val frame = CharArray(baseLength) { launch {
encryptedChars[Random.nextInt(encryptedChars.size)] delay(index * 50L) // Stagger start like JS version
// Animate this character indefinitely in a loop
while (true) {
// Animate with random characters
while (characterStates.getOrNull(index) == CharacterAnimationState.ENCRYPTED) {
// Generate random encrypted character for this position
val newContent = animatedContent.toCharArray()
if (index < newContent.size) {
newContent[index] = encryptedChars[Random.nextInt(encryptedChars.size)]
animatedContent = String(newContent)
}
delay(100L) // Change character every 100ms like JS
// Random chance to reveal (10% like JS version)
if (Random.nextFloat() < 0.1f) {
// Reveal the final character
val finalContent = animatedContent.toCharArray()
if (index < finalContent.size) {
finalContent[index] = targetChar
animatedContent = String(finalContent)
}
// Mark as revealed
val finalStates = characterStates.toMutableList()
finalStates[index] = CharacterAnimationState.FINAL
characterStates = finalStates
break
}
}
// Keep revealed for 2 seconds, then fade back to encrypted (like JS)
delay(2000L)
// Reset back to encrypted for next cycle
val resetStates = characterStates.toMutableList()
resetStates[index] = CharacterAnimationState.ENCRYPTED
characterStates = resetStates
}
}
} }
animatedContent = String(frame)
// Small delay for smooth animation; adjust if needed
delay(80L)
} }
} else { } else {
// Not animating, show final content // Not animating, show final content
animatedContent = message.content animatedContent = message.content
characterStates = message.content.map { CharacterAnimationState.FINAL }
} }
} }