Add a fadeout delay to the ribbon dice.

This commit is contained in:
Thomas Andres Gomez 2025-05-09 14:14:13 +02:00
parent 7e1ce3ba01
commit 06c5802d7a
10 changed files with 97 additions and 24 deletions

View file

@ -262,6 +262,8 @@
<string name="settings__player_portrait__section">Portrait joueur</string>
<string name="settings__player_portrait__dyn_dice_tile">Dés dynamiques</string>
<string name="settings__player_portrait__dyn_dice_description">Affiche un dé à côté du portrait d'un personnage lorsqu'un jet est fait par ce dernier.</string>
<string name="settings__player_portrait__dyn_dice_delay_tile">Délai pour les Dés dynamiques</string>
<string name="settings__player_portrait__dyn_dice_delay_description">Délai après lequel les dés dynamiques disparaissent.</string>
<string name="settings__chat_log__section">Chatlog options</string>
<string name="settings__chat_log__auto_show_title">Afficher automatiquement le chat</string>
<string name="settings__chat_log__auto_show_description">Affiche automatiquement le chat lors de la réception d'un message</string>

View file

@ -17,6 +17,7 @@ class SettingsFactory(
port = settings.network.port,
playerName = settings.playerName,
dynamicDice = settings.portrait.dynamicDice,
dynamicDiceDelay = settings.portrait.dynamicDiceDelay,
autoHideChat = settings.chat.autoHideChat,
autoHideDelay = settings.chat.autoHideDelay,
autoShowChat = settings.chat.autoShowChat,
@ -47,6 +48,7 @@ class SettingsFactory(
),
portrait = Settings.Portrait(
dynamicDice = json.dynamicDice ?: default.portrait.dynamicDice,
dynamicDiceDelay = json.dynamicDiceDelay ?: default.portrait.dynamicDiceDelay,
),
chat = Settings.Chat(
autoHideChat = json.autoHideChat ?: default.chat.autoHideChat,

View file

@ -10,6 +10,7 @@ data class Settings(
) {
data class Portrait(
val dynamicDice: Boolean,
val dynamicDiceDelay: Int,
)
data class Chat(

View file

@ -8,6 +8,7 @@ data class SettingsJsonV1(
val port: Int?,
val playerName: String?,
val dynamicDice: Boolean?,
val dynamicDiceDelay: Int?,
val autoHideChat: Boolean?,
val autoHideDelay: Int?,
val autoShowChat: Boolean?,

View file

@ -136,6 +136,8 @@ abstract class CharacterRibbonViewModel(
) { settings, roll ->
if (settings.portrait.dynamicDice && characterSheetId == roll.characterSheetId) {
state.value = CharacterRibbonRollUio(
rollId = roll.uuid,
hideDelay = settings.portrait.dynamicDiceDelay,
characterSheetId = characterSheetId,
value = roll.rollValue,
tint = when (roll.critical) {
@ -153,10 +155,4 @@ abstract class CharacterRibbonViewModel(
return state
}
fun onPortraitRollRightClick(
characterSheetId: String,
) {
rolls[characterSheetId]?.value = null
}
}

View file

@ -5,8 +5,10 @@ import androidx.compose.animation.SizeTransform
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.AnimationVector1D
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.togetherWith
@ -40,16 +42,29 @@ import org.jetbrains.compose.resources.painterResource
@Stable
data class CharacterRibbonRollUio(
val rollId: String,
val hideDelay: Int,
val characterSheetId: String,
val value: Int?,
val tint: Color?,
)
@Stable
data class CharacterRibbonRollAnimation(
val rotation: Animatable<Float, AnimationVector1D> = Animatable(0f),
val scale: Animatable<Float, AnimationVector1D> = Animatable(1f),
)
class CharacterRibbonRollAnimation(
val animatedAlpha: Animatable<Float, AnimationVector1D>,
val animatedRotation: Animatable<Float, AnimationVector1D>,
val animatedScale: Animatable<Float, AnimationVector1D>,
) {
constructor(
alpha: Float = 1f,
rotation: Float = 0f,
scale: Float = 1f,
) : this(
animatedAlpha = Animatable(alpha),
animatedRotation = Animatable(rotation),
animatedScale = Animatable(scale),
)
}
@Composable
fun CharacterRibbonRoll(
@ -69,13 +84,17 @@ fun CharacterRibbonRoll(
enter togetherWith exit using SizeTransform(clip = false)
}
) {
val animation = diceIconAnimation(key = it ?: Unit)
val animation = diceIconAnimation(
key = it?.rollId ?: Unit,
rollDelay = it?.hideDelay ?: 1000,
)
val color = animateColorAsState(targetValue = it?.tint ?: Color.Transparent)
Box(
modifier = Modifier.graphicsLayer {
this.scaleX = animation.scale.value
this.scaleY = animation.scale.value
this.alpha = animation.animatedAlpha.value
this.scaleX = animation.animatedScale.value
this.scaleY = animation.animatedScale.value
},
contentAlignment = Alignment.Center,
) {
@ -83,7 +102,7 @@ fun CharacterRibbonRoll(
Icon(
modifier = Modifier
.graphicsLayer {
this.rotationZ = animation.rotation.value
this.rotationZ = animation.animatedRotation.value
}
.fillMaxWidth()
.aspectRatio(1f)
@ -111,20 +130,23 @@ fun CharacterRibbonRoll(
}
@Composable
private fun diceIconAnimation(key: Any = Unit): CharacterRibbonRollAnimation {
private fun diceIconAnimation(
key: Any = Unit,
rollDelay: Int,
): CharacterRibbonRollAnimation {
val animation = remember(key) {
CharacterRibbonRollAnimation()
}
LaunchedEffect(key) {
launch {
animation.scale.animateTo(
animation.animatedScale.animateTo(
targetValue = 1.20f,
animationSpec = spring(
dampingRatio = Spring.DampingRatioNoBouncy,
stiffness = 800f,
)
)
animation.scale.animateTo(
animation.animatedScale.animateTo(
targetValue = 1f,
animationSpec = spring(
dampingRatio = 0.28f,
@ -133,7 +155,7 @@ private fun diceIconAnimation(key: Any = Unit): CharacterRibbonRollAnimation {
)
}
launch {
animation.rotation.animateTo(
animation.animatedRotation.animateTo(
targetValue = 360f * 3,
animationSpec = spring(
dampingRatio = Spring.DampingRatioNoBouncy,
@ -141,6 +163,16 @@ private fun diceIconAnimation(key: Any = Unit): CharacterRibbonRollAnimation {
)
)
}
launch {
animation.animatedAlpha.animateTo(
targetValue = 0f,
animationSpec = tween(
delayMillis = rollDelay,
durationMillis = 2000,
easing = FastOutSlowInEasing,
)
)
}
}
return animation
}

View file

@ -18,6 +18,7 @@ import androidx.compose.material.TopAppBar
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.KeyEventType
@ -42,8 +43,11 @@ import lwacharactersheet.composeapp.generated.resources.settings__title
import org.jetbrains.compose.resources.stringResource
import org.koin.compose.viewmodel.koinViewModel
@Stable
object SettingsScreenDefault {
@Stable
val margin: PaddingValues = PaddingValues(horizontal = 16.dp)
@Stable
val padding: PaddingValues = PaddingValues(start = 16.dp, top = 8.dp, end = 8.dp, bottom = 8.dp)
}

View file

@ -25,12 +25,14 @@ import lwacharactersheet.composeapp.generated.resources.settings__chat_log__auto
import lwacharactersheet.composeapp.generated.resources.settings__chat_log__auto_scroll_title
import lwacharactersheet.composeapp.generated.resources.settings__chat_log__auto_show_description
import lwacharactersheet.composeapp.generated.resources.settings__chat_log__auto_show_title
import lwacharactersheet.composeapp.generated.resources.settings__chat_log__line_count_description
import lwacharactersheet.composeapp.generated.resources.settings__chat_log__line_count_title
import lwacharactersheet.composeapp.generated.resources.settings__chat_log__section
import lwacharactersheet.composeapp.generated.resources.settings__player_portrait__dyn_dice_delay_description
import lwacharactersheet.composeapp.generated.resources.settings__player_portrait__dyn_dice_delay_tile
import lwacharactersheet.composeapp.generated.resources.settings__player_portrait__dyn_dice_description
import lwacharactersheet.composeapp.generated.resources.settings__player_portrait__dyn_dice_tile
import lwacharactersheet.composeapp.generated.resources.settings__player_portrait__section
import lwacharactersheet.composeapp.generated.resources.settings__chat_log__line_count_title
import lwacharactersheet.composeapp.generated.resources.settings__chat_log__line_count_description
class SettingsViewModel(
@ -58,6 +60,35 @@ class SettingsViewModel(
)
},
),
SettingNumberItemUio(
icon = Res.drawable.ic_timer_24dp,
title = Res.string.settings__player_portrait__dyn_dice_delay_tile,
description = Res.string.settings__player_portrait__dyn_dice_delay_description,
enable = booleanStates.dynamicDice,
value = intStates.dynamicDiceDelay,
onValueChange = {
val range = 1000..99999
if (it < range.first) {
settingsRepository.update(
settings = settings.value.copy(
portrait = settings.value.portrait.copy(dynamicDiceDelay = range.first)
)
)
} else if (it in range) {
settingsRepository.update(
settings = settings.value.copy(
portrait = settings.value.portrait.copy(dynamicDiceDelay = it)
)
)
} else {
settingsRepository.update(
settings = settings.value.copy(
portrait = settings.value.portrait.copy(dynamicDiceDelay = range.last)
)
)
}
}
),
SettingSectionUio(
title = Res.string.settings__chat_log__section,
),
@ -138,6 +169,7 @@ class SettingsViewModel(
viewModelScope.launch {
settingsRepository.settingsFlow().collect { settings ->
booleanStates.dynamicDice.value = settings.portrait.dynamicDice
intStates.dynamicDiceDelay.value = settings.portrait.dynamicDiceDelay
booleanStates.autoShowChat.value = settings.chat.autoShowChat
booleanStates.autoHideChat.value = settings.chat.autoHideChat
intStates.autoHideDelay.value = settings.chat.autoHideDelay
@ -162,6 +194,9 @@ class SettingsViewModel(
private val HashMap<String, MutableState<Boolean>>.dynamicDice
get() = getOrPut("DYNAMIC_DICE") { mutableStateOf(settings.value.portrait.dynamicDice) }
private val HashMap<String, MutableState<Int>>.dynamicDiceDelay
get() = getOrPut("DYNAMIC_DICE_DELAY") { mutableStateOf(settings.value.portrait.dynamicDiceDelay) }
private val HashMap<String, MutableState<Boolean>>.autoShowChat
get() = getOrPut("AUTO_SHOW_CHAT") { mutableStateOf(settings.value.chat.autoShowChat) }

View file

@ -114,7 +114,7 @@ fun SettingNumberItem(
BasicTextField(
modifier = Modifier
.onFocusChanged { focused.value = it.isFocused }
.width(width = 44.dp)
.width(width = 64.dp)
.padding(horizontal = 2.dp),
textStyle = MaterialTheme.lwa.typography.settings.input.copy(
color = textColor.value,
@ -129,10 +129,9 @@ fun SettingNumberItem(
Box(
modifier = Modifier
.background(color = borderColor.value)
.size(width = 48.dp, height = 2.dp),
.size(width = 64.dp, height = 2.dp),
)
}
}
}
}

View file

@ -7,7 +7,8 @@ class SettingsUseCase {
fun defaultSettings(): Settings = Settings(
playerName = "",
portrait = Settings.Portrait(
dynamicDice = true
dynamicDice = true,
dynamicDiceDelay = 5000,
),
chat = Settings.Chat(
autoHideChat = true,