Refactor the Character Ribbon (levelup)

This commit is contained in:
Thomas Andres Gomez 2025-10-18 16:06:38 +02:00
parent 24fe030663
commit 6364201c6c
14 changed files with 457 additions and 331 deletions

View file

@ -38,8 +38,9 @@ kotlin {
// composable component. // composable component.
implementation(libs.coil.compose) implementation(libs.coil.compose)
implementation(libs.coil.network.ktor) implementation(libs.coil.network.ktor)
// implementation("com.mikepenz.hypnoticcanvas:hypnoticcanvas:0.3.0") // Shader
// implementation("com.mikepenz.hypnoticcanvas:hypnoticcanvas-shaders:0.3.0") implementation(libs.hypnoticcanvas)
implementation(libs.hypnoticcanvas.shaders)
// network // network
implementation(libs.kotlinx.serialization.json) implementation(libs.kotlinx.serialization.json)
implementation(libs.ktor.serialization.json) implementation(libs.ktor.serialization.json)

View file

@ -19,6 +19,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import com.pixelized.desktop.lwa.LocalBlurController import com.pixelized.desktop.lwa.LocalBlurController
import com.pixelized.desktop.lwa.ui.composable.blur.BlurContentController import com.pixelized.desktop.lwa.ui.composable.blur.BlurContentController
import com.pixelized.desktop.lwa.ui.composable.decoratedBox.DecoratedBox import com.pixelized.desktop.lwa.ui.composable.decoratedBox.DecoratedBox
@ -32,6 +33,7 @@ object LwaDialogDefault {
@Composable @Composable
fun <T> LwaDialog( fun <T> LwaDialog(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
properties: DialogProperties = DialogProperties(),
blur: BlurContentController? = LocalBlurController.current, blur: BlurContentController? = LocalBlurController.current,
paddings: PaddingValues = LwaDialogDefault.paddings, paddings: PaddingValues = LwaDialogDefault.paddings,
color: Color = MaterialTheme.colors.surface, color: Color = MaterialTheme.colors.surface,
@ -52,6 +54,7 @@ fun <T> LwaDialog(
} }
Dialog( Dialog(
properties = properties,
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
content = { content = {
Box( Box(

View file

@ -6,7 +6,6 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
@ -19,22 +18,21 @@ import androidx.compose.material.Surface
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.material.minimumInteractiveComponentSize import androidx.compose.material.minimumInteractiveComponentSize
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import androidx.compose.runtime.State import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.DialogProperties
import com.pixelized.desktop.lwa.LocalBlurController import androidx.compose.ui.zIndex
import com.pixelized.desktop.lwa.ui.composable.blur.BlurContentController import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.pixelized.desktop.lwa.ui.composable.character.LwaDialog
import com.pixelized.desktop.lwa.ui.composable.textfield.LwaTextFieldUio import com.pixelized.desktop.lwa.ui.composable.textfield.LwaTextFieldUio
import com.pixelized.desktop.lwa.ui.screen.gamemaster.common.GMFilterHeader import com.pixelized.desktop.lwa.ui.screen.gamemaster.common.GMFilterHeader
import com.pixelized.desktop.lwa.ui.screen.gamemaster.common.tag.GMTagUio import com.pixelized.desktop.lwa.ui.screen.gamemaster.common.tag.GMTagUio
import com.pixelized.desktop.lwa.ui.theme.lwa import com.pixelized.desktop.lwa.ui.theme.lwa
import com.pixelized.desktop.lwa.utils.extention.calculatePaddings
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import lwacharactersheet.composeapp.generated.resources.Res import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.ic_close_24dp import lwacharactersheet.composeapp.generated.resources.ic_close_24dp
@ -52,29 +50,19 @@ data class CharacterSheetAlterationDialogUio(
@OptIn(ExperimentalComposeUiApi::class) @OptIn(ExperimentalComposeUiApi::class)
@Composable @Composable
fun CharacterSheetAlterationDialog( fun CharacterSheetAlterationDialog(
blur: BlurContentController? = LocalBlurController.current,
dialog: State<CharacterSheetAlterationDialogUio?>, dialog: State<CharacterSheetAlterationDialogUio?>,
onTag: (String) -> Unit, onTag: (String) -> Unit,
onAlteration: (characterSheetId: String, alterationId: String, active: Boolean) -> Unit, onAlteration: (characterSheetId: String, alterationId: String, active: Boolean) -> Unit,
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
) { ) {
dialog.value?.let { LwaDialog(
blur?.let {
DisposableEffect("LwaDialog") {
blur.show()
onDispose {
blur.hide()
}
}
}
Dialog(
properties = DialogProperties( properties = DialogProperties(
usePlatformDefaultWidth = false, usePlatformDefaultWidth = false,
usePlatformInsets = false, usePlatformInsets = false,
), ),
state = dialog,
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
onConfirm = onDismissRequest,
content = { content = {
CharacterSheetAlterationContent( CharacterSheetAlterationContent(
dialog = it, dialog = it,
@ -85,26 +73,33 @@ fun CharacterSheetAlterationDialog(
} }
) )
} }
}
@Composable @Composable
fun CharacterSheetAlterationContent( fun CharacterSheetAlterationContent(
dialog: CharacterSheetAlterationDialogUio, dialog: CharacterSheetAlterationDialogUio,
paddingValues: PaddingValues = MaterialTheme.lwa.dimen.paddingValues,
onTag: (String) -> Unit, onTag: (String) -> Unit,
onAlteration: (characterSheetId: String, alterationId: String, active: Boolean) -> Unit, onAlteration: (characterSheetId: String, alterationId: String, active: Boolean) -> Unit,
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
) { ) {
Surface( val padding = MaterialTheme.lwa.dimen.paddingValue
val (start, _, end, _) = paddingValues.calculatePaddings()
Column(
modifier = Modifier modifier = Modifier
.fillMaxHeight() .fillMaxHeight()
.width(width = 128.dp * 4) .width(width = 128.dp * 4)
.padding(vertical = 16.dp),
) { ) {
Column( // Add a surface and a zIndex() to avoid the animation coming from : .animateItem() (see below)
modifier = Modifier.fillMaxSize(), // to draw over this part of the dialog.
Surface(
modifier = Modifier.zIndex(1f)
) { ) {
Column {
Row( Row(
modifier = Modifier.padding(start = 16.dp).fillMaxWidth(), modifier = Modifier
.padding(start = start)
.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
) { ) {
@ -125,19 +120,22 @@ fun CharacterSheetAlterationContent(
} }
GMFilterHeader( GMFilterHeader(
filter = dialog.filter, filter = dialog.filter,
tags = dialog.tags.collectAsState(), padding = padding,
tags = dialog.tags.collectAsStateWithLifecycle(),
onTag = onTag, onTag = onTag,
) )
}
}
LazyColumn( LazyColumn(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.weight(weight = 1f), .weight(weight = 1f, fill = true),
contentPadding = PaddingValues(start = 8.dp, end = 8.dp, bottom = 8.dp), contentPadding = paddingValues,
verticalArrangement = Arrangement.spacedBy(space = 8.dp), verticalArrangement = Arrangement.spacedBy(space = padding),
) { ) {
items( items(
items = dialog.alterations, items = dialog.alterations,
key = { it.id } key = { it.id },
) { alteration -> ) { alteration ->
AlterationToggleItem( AlterationToggleItem(
modifier = Modifier modifier = Modifier
@ -147,7 +145,7 @@ fun CharacterSheetAlterationContent(
shape = MaterialTheme.lwa.shapes.base.small, shape = MaterialTheme.lwa.shapes.base.small,
) )
.minimumInteractiveComponentSize() .minimumInteractiveComponentSize()
.padding(horizontal = 8.dp), .padding(start = start, end = end),
alteration = alteration, alteration = alteration,
onAlteration = { onAlteration = {
onAlteration( onAlteration(
@ -162,4 +160,3 @@ fun CharacterSheetAlterationContent(
} }
} }
} }
}

View file

@ -34,7 +34,6 @@ import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import com.pixelized.desktop.lwa.ui.composable.blur.BlurContent import com.pixelized.desktop.lwa.ui.composable.blur.BlurContent
import com.pixelized.desktop.lwa.ui.composable.blur.rememberBlurContentController import com.pixelized.desktop.lwa.ui.composable.blur.rememberBlurContentController
import com.pixelized.desktop.lwa.ui.composable.character.alterteration.CharacterSheetAlterationDialog import com.pixelized.desktop.lwa.ui.composable.character.alterteration.CharacterSheetAlterationDialog
@ -143,9 +142,6 @@ fun CampaignScreen(
) )
} }
}, },
onLevelUp = {
screen.navigateToLevelScreen(characterSheetId = it)
}
) )
}, },
rightPanel = { rightPanel = {
@ -169,9 +165,6 @@ fun CampaignScreen(
) )
} }
}, },
onLevelUp = {
screen.navigateToLevelScreen(characterSheetId = it)
}
) )
}, },
leftOverlay = { leftOverlay = {

View file

@ -1,10 +1,16 @@
package com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon package com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.PointerMatcher import androidx.compose.foundation.PointerMatcher
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.onClick import androidx.compose.foundation.onClick
@ -13,13 +19,22 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.PointerButton import androidx.compose.ui.input.pointer.PointerButton
import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import com.lordcodes.turtle.Arguments
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.common.CharacterRibbonAlteration import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.common.CharacterRibbonAlteration
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.common.CharacterRibbonPortrait import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.common.CharacterRibbonPortrait
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.common.CharacterRibbonRoll import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.common.CharacterRibbonRoll
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.common.CharacterRibbonStats
import com.pixelized.desktop.lwa.ui.shaders.packs.GoldenFlow
import com.pixelized.desktop.lwa.ui.shaders.shaderBorder
import com.pixelized.desktop.lwa.ui.theme.lwa import com.pixelized.desktop.lwa.ui.theme.lwa
import com.pixelized.desktop.lwa.utils.extention.invert import com.pixelized.desktop.lwa.utils.extention.invert
@ -29,10 +44,11 @@ fun CharacterRibbon(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
layoutDirection: LayoutDirection, layoutDirection: LayoutDirection,
viewModel: CharacterRibbonViewModel, viewModel: CharacterRibbonViewModel,
shape: Shape = MaterialTheme.lwa.shapes.portrait,
size: DpSize = MaterialTheme.lwa.dimen.portrait.minimized,
padding: PaddingValues = MaterialTheme.lwa.dimen.paddingValues, padding: PaddingValues = MaterialTheme.lwa.dimen.paddingValues,
onCharacterLeftClick: (characterSheetId: String) -> Unit, onCharacterLeftClick: (characterSheetId: String) -> Unit,
onCharacterRightClick: (characterSheetId: String) -> Unit, onCharacterRightClick: (characterSheetId: String) -> Unit,
onLevelUp: (characterSheetId: String) -> Unit,
) { ) {
val characters = viewModel.characters.collectAsState() val characters = viewModel.characters.collectAsState()
@ -53,6 +69,7 @@ fun CharacterRibbon(
Box( Box(
modifier = Modifier modifier = Modifier
.animateItem() .animateItem()
.clip(shape = shape)
.graphicsLayer { if (it.hideOverruled) this.alpha = 0.3f } .graphicsLayer { if (it.hideOverruled) this.alpha = 0.3f }
.onClick( .onClick(
matcher = PointerMatcher.mouse(PointerButton.Primary), matcher = PointerMatcher.mouse(PointerButton.Primary),
@ -66,19 +83,38 @@ fun CharacterRibbon(
), ),
) { ) {
CharacterRibbonPortrait( CharacterRibbonPortrait(
size = size,
character = it.portrait, character = it.portrait,
onLevelUp = { onLevelUp(it.characterSheetId) },
) )
Column(
modifier = Modifier.size(size),
verticalArrangement = Arrangement.SpaceBetween,
) {
CompositionLocalProvider( CompositionLocalProvider(
LocalLayoutDirection provides layoutDirection.invert LocalLayoutDirection provides layoutDirection.invert
) { ) {
CharacterRibbonAlteration( CharacterRibbonAlteration(
alterations = it.alterations,
)
}
CharacterRibbonStats(
status = it.status, status = it.status,
) )
} }
CharacterRibbonRoll( CharacterRibbonRoll(
value = viewModel.roll(characterSheetId = it.characterSheetId).value, value = viewModel.roll(characterSheetId = it.characterSheetId).value,
) )
AnimatedVisibility(
visible = it.levelUp,
enter = fadeIn(),
exit = fadeOut()
) {
Box(
modifier = Modifier
.size(size)
.shaderBorder(width = 2.dp, shape = shape, shader = GoldenFlow)
)
}
} }
} }
} }

View file

@ -3,6 +3,8 @@ package com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon
import com.pixelized.desktop.lwa.ui.composable.tooltip.BasicTooltipUio import com.pixelized.desktop.lwa.ui.composable.tooltip.BasicTooltipUio
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.common.CharacterRibbonAlterationUio import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.common.CharacterRibbonAlterationUio
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.common.CharacterRibbonPortraitUio import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.common.CharacterRibbonPortraitUio
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.common.CharacterRibbonStats
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.common.CharacterRibbonStatsUio
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.common.CharacterRibbonUio import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.common.CharacterRibbonUio
import com.pixelized.desktop.lwa.utils.extention.foldInto import com.pixelized.desktop.lwa.utils.extention.foldInto
import com.pixelized.shared.lwa.model.AlteredCharacterSheetFactory import com.pixelized.shared.lwa.model.AlteredCharacterSheetFactory
@ -35,20 +37,25 @@ class CharacterRibbonFactory(
characterSheetId = characterSheet.id, characterSheetId = characterSheet.id,
hideOverruled = hideOverruled, hideOverruled = hideOverruled,
enableDetail = enableCharacterSheet, enableDetail = enableCharacterSheet,
levelUp = alteredCharacterSheet.shouldLevelUp,
portrait = CharacterRibbonPortraitUio( portrait = CharacterRibbonPortraitUio(
portrait = alteredCharacterSheet.thumbnail, portrait = alteredCharacterSheet.thumbnail,
name = alteredCharacterSheet.name, name = alteredCharacterSheet.name,
levelUp = alteredCharacterSheet.shouldLevelUp, damagePercent = takeIf { enableCharacterStats }?.let {
stats = takeIf { enableCharacterStats }?.let { val maxHp = alteredCharacterSheet.maxHp.toFloat()
CharacterRibbonPortraitUio.StatsDetail( val damage = alteredCharacterSheet.damage.toFloat()
1f - ((maxHp - damage) / maxHp).coerceIn(0f, 1f)
},
),
status = takeIf { enableCharacterStats }?.let {
CharacterRibbonStatsUio(
hp = alteredCharacterSheet.maxHp - alteredCharacterSheet.damage, hp = alteredCharacterSheet.maxHp - alteredCharacterSheet.damage,
maxHp = alteredCharacterSheet.maxHp, maxHp = alteredCharacterSheet.maxHp,
pp = alteredCharacterSheet.maxPp - alteredCharacterSheet.fatigue, pp = alteredCharacterSheet.maxPp - alteredCharacterSheet.fatigue,
maxPp = alteredCharacterSheet.maxPp, maxPp = alteredCharacterSheet.maxPp,
) )
}, },
), alterations = takeIf { enableCharacterStatus }?.let {
status = takeIf { enableCharacterStatus }?.let {
alterations.map { alteration -> alterations.map { alteration ->
CharacterRibbonAlterationUio( CharacterRibbonAlterationUio(
icon = alteration.metadata.icon ?: DEFAULT_ICON, icon = alteration.metadata.icon ?: DEFAULT_ICON,

View file

@ -13,22 +13,19 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import com.pixelized.desktop.lwa.ui.theme.lwa import com.pixelized.desktop.lwa.ui.theme.lwa
import kotlin.math.max
import kotlin.math.min
@Composable @Composable
fun BloodOverlay( fun BloodOverlay(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
bloodColor: Color = MaterialTheme.lwa.colorScheme.portrait.blood, bloodColor: Color = MaterialTheme.lwa.colorScheme.portrait.blood,
maxHp: Float, damagePercent: Float,
hp: Float,
) { ) {
val animatedRatio = animateFloatAsState( val animatedRatio = animateFloatAsState(
targetValue = min(maxHp, max(0f, (maxHp - hp) / maxHp)), targetValue = damagePercent,
animationSpec = tween(durationMillis = 350, easing = EaseOutCirc) animationSpec = tween(durationMillis = 350, easing = EaseOutCirc)
) )
val animatedColor = animateColorAsState( val animatedColor = animateColorAsState(
targetValue = bloodColor.copy(alpha = ((maxHp - hp) / maxHp) / 4f + .25f) targetValue = bloodColor.copy(alpha = damagePercent / 4f + .25f)
) )
Box( Box(
modifier = modifier modifier = modifier

View file

@ -1,10 +1,5 @@
package com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.common package com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.common
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.animateContentSize
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.TooltipPlacement import androidx.compose.foundation.TooltipPlacement
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
@ -42,19 +37,18 @@ data class CharacterRibbonAlterationUio(
fun CharacterRibbonAlteration( fun CharacterRibbonAlteration(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
width: Dp = MaterialTheme.lwa.dimen.portrait.minimized.width, width: Dp = MaterialTheme.lwa.dimen.portrait.minimized.width,
status: List<List<CharacterRibbonAlterationUio>>, alterations: List<List<CharacterRibbonAlterationUio>>,
) { ) {
val direction = LocalLayoutDirection.current val direction = LocalLayoutDirection.current
Row( Row(
modifier = Modifier modifier = Modifier
.animateContentSize()
.width(width = width) .width(width = width)
.then(other = modifier), .then(other = modifier),
horizontalArrangement = Arrangement.spacedBy(space = 4.dp), horizontalArrangement = Arrangement.spacedBy(space = 4.dp),
) { ) {
status.forEach { columns -> alterations.forEach { columns ->
Column( Column(
modifier = Modifier.animateContentSize(),
verticalArrangement = Arrangement.spacedBy(space = 2.dp), verticalArrangement = Arrangement.spacedBy(space = 2.dp),
) { ) {
columns.forEach { columns.forEach {
@ -70,21 +64,16 @@ fun CharacterRibbonAlteration(
) )
}, },
content = { content = {
AnimatedContent(
targetState = it.icon,
transitionSpec = { fadeIn() togetherWith fadeOut() },
) { icon ->
LwaAsyncImage( LwaAsyncImage(
modifier = Modifier.size(24.dp), modifier = Modifier.size(24.dp),
model = ImageRequest.Builder(context = PlatformContext.INSTANCE) model = ImageRequest.Builder(context = PlatformContext.INSTANCE)
.data(data = icon) .data(data = it.icon)
.size(size = 48) .size(size = 48)
.build(), .build(),
filterQuality = FilterQuality.High, filterQuality = FilterQuality.High,
contentDescription = null, contentDescription = null,
) )
} }
}
) )
} }
} }

View file

@ -1,77 +1,54 @@
package com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.common package com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.common
import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
import androidx.compose.animation.togetherWith import androidx.compose.animation.togetherWith
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawWithContent import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.FilterQuality import androidx.compose.ui.graphics.FilterQuality
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import com.pixelized.desktop.lwa.ui.composable.image.LwaAsyncImage import com.pixelized.desktop.lwa.ui.composable.image.LwaAsyncImage
import com.pixelized.desktop.lwa.ui.composable.shapes.ArrowShape
import com.pixelized.desktop.lwa.ui.theme.lwa import com.pixelized.desktop.lwa.ui.theme.lwa
import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.ic_heart_24dp
import lwacharactersheet.composeapp.generated.resources.ic_water_drop_24dp
import org.jetbrains.compose.resources.painterResource
@Stable @Stable
data class CharacterRibbonPortraitUio( data class CharacterRibbonPortraitUio(
val portrait: String?, val portrait: String?,
val name: String, val name: String,
val levelUp: Boolean, val damagePercent: Float?,
val stats: StatsDetail?,
) {
@Stable
data class StatsDetail(
val hp: Int,
val maxHp: Int,
val pp: Int,
val maxPp: Int,
) )
}
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun CharacterRibbonPortrait( fun CharacterRibbonPortrait(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
background: Color = MaterialTheme.lwa.colorScheme.elevated.base1dp,
overlay: Brush = MaterialTheme.lwa.colorScheme.portraitBackgroundBrush,
size: DpSize = MaterialTheme.lwa.dimen.portrait.minimized, size: DpSize = MaterialTheme.lwa.dimen.portrait.minimized,
levelUpOffset: Dp = 9.dp,
character: CharacterRibbonPortraitUio, character: CharacterRibbonPortraitUio,
onLevelUp: () -> Unit,
) { ) {
val colorScheme = MaterialTheme.lwa.colorScheme
Box( Box(
modifier = modifier modifier = modifier
.size(size = size) .size(size = size)
.clip(shape = MaterialTheme.lwa.shapes.portrait) .background(color = background)
.background(color = colorScheme.elevated.base1dp) .drawWithContent {
drawContent()
drawRect(brush = overlay)
}
) { ) {
AnimatedContent( AnimatedContent(
targetState = character.portrait, targetState = character.portrait,
@ -87,95 +64,10 @@ fun CharacterRibbonPortrait(
) )
} }
AnimatedVisibility( character.damagePercent?.let {
modifier = Modifier
.align(alignment = Alignment.TopEnd)
.offset(x = levelUpOffset, y = -levelUpOffset),
visible = character.levelUp,
enter = fadeIn(),
exit = fadeOut(),
) {
IconButton(
onClick = onLevelUp,
) {
ArrowShape(
color = MaterialTheme.lwa.colorScheme.portrait.levelUp,
)
}
}
character.stats?.let { stats ->
BloodOverlay( BloodOverlay(
maxHp = stats.maxHp.toFloat(), damagePercent = it,
hp = stats.hp.toFloat(),
)
Column(
modifier = Modifier
.fillMaxSize()
.drawWithContent {
drawRect(brush = colorScheme.portraitBackgroundBrush)
drawContent()
}
.padding(vertical = 2.dp, horizontal = 4.dp),
verticalArrangement = Arrangement.aligned(alignment = Alignment.Bottom),
) {
Row {
Icon(
modifier = Modifier
.size(12.dp)
.offset(y = 2.dp),
painter = painterResource(Res.drawable.ic_heart_24dp),
contentDescription = null
)
Spacer(
modifier = Modifier.width(width = 2.dp),
)
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.lwa.typography.portrait.value,
text = "${stats.hp}",
)
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.lwa.typography.portrait.separator,
text = "/",
)
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.lwa.typography.portrait.max,
text = "${stats.maxHp}",
)
}
Row {
Icon(
modifier = Modifier
.size(12.dp)
.offset(y = 2.dp),
painter = painterResource(Res.drawable.ic_water_drop_24dp),
contentDescription = null
)
Spacer(
modifier = Modifier.width(width = 2.dp),
)
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.lwa.typography.portrait.value,
text = "${stats.pp}",
)
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.lwa.typography.portrait.separator,
text = "/",
)
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.lwa.typography.portrait.max,
text = "${stats.maxPp}",
) )
} }
} }
} }
}
}

View file

@ -0,0 +1,104 @@
package com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.common
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.pixelized.desktop.lwa.ui.theme.lwa
import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.ic_heart_24dp
import lwacharactersheet.composeapp.generated.resources.ic_water_drop_24dp
import org.jetbrains.compose.resources.painterResource
@Stable
data class CharacterRibbonStatsUio(
val hp: Int,
val maxHp: Int,
val pp: Int,
val maxPp: Int,
)
@Composable
fun CharacterRibbonStats(
modifier: Modifier = Modifier,
status: CharacterRibbonStatsUio?,
) {
status?.let { status ->
Column(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 2.dp, horizontal = 4.dp)
.then(other = modifier),
verticalArrangement = Arrangement.aligned(alignment = Alignment.Bottom),
) {
Row {
Icon(
modifier = Modifier
.size(12.dp)
.offset(y = 2.dp),
painter = painterResource(Res.drawable.ic_heart_24dp),
contentDescription = null
)
Spacer(
modifier = Modifier.width(width = 2.dp),
)
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.lwa.typography.portrait.value,
text = "${status.hp}",
)
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.lwa.typography.portrait.separator,
text = "/",
)
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.lwa.typography.portrait.max,
text = "${status.maxHp}",
)
}
Row {
Icon(
modifier = Modifier
.size(size = 12.dp)
.offset(y = 2.dp),
painter = painterResource(Res.drawable.ic_water_drop_24dp),
contentDescription = null
)
Spacer(
modifier = Modifier.width(width = 2.dp),
)
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.lwa.typography.portrait.value,
text = "${status.pp}",
)
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.lwa.typography.portrait.separator,
text = "/",
)
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.lwa.typography.portrait.max,
text = "${status.maxPp}",
)
}
}
}
}

View file

@ -7,6 +7,8 @@ class CharacterRibbonUio(
val characterSheetId: String, val characterSheetId: String,
val hideOverruled: Boolean, val hideOverruled: Boolean,
val enableDetail: Boolean, val enableDetail: Boolean,
val levelUp: Boolean,
val portrait: CharacterRibbonPortraitUio, val portrait: CharacterRibbonPortraitUio,
val status: List<List<CharacterRibbonAlterationUio>>, val status: CharacterRibbonStatsUio?,
val alterations: List<List<CharacterRibbonAlterationUio>>,
) )

View file

@ -1,67 +1,125 @@
//package com.pixelized.desktop.lwa.ui.shaders package com.pixelized.desktop.lwa.ui.shaders
//
//import androidx.compose.animation.core.withInfiniteAnimationFrameMillis import androidx.compose.animation.core.withInfiniteAnimationFrameMillis
//import androidx.compose.runtime.Composable import androidx.compose.foundation.border
//import androidx.compose.runtime.getValue import androidx.compose.runtime.Composable
//import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.DisposableEffect
//import androidx.compose.runtime.produceState import androidx.compose.runtime.LaunchedEffect
//import androidx.compose.runtime.remember import androidx.compose.runtime.getValue
//import androidx.compose.runtime.setValue import androidx.compose.runtime.mutableStateOf
//import androidx.compose.ui.Modifier import androidx.compose.runtime.produceState
//import androidx.compose.ui.draw.drawBehind import androidx.compose.runtime.remember
//import androidx.compose.ui.draw.drawWithContent import androidx.compose.runtime.setValue
//import androidx.compose.ui.geometry.Size import androidx.compose.ui.Modifier
//import androidx.compose.ui.graphics.BlendMode import androidx.compose.ui.draw.drawWithContent
//import androidx.compose.ui.graphics.Brush import androidx.compose.ui.geometry.Size
//import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.BlendMode
//import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.graphics.Brush
//import com.mikepenz.hypnoticcanvas.NonAndroidRuntimeEffect import androidx.compose.ui.graphics.Color
//import com.mikepenz.hypnoticcanvas.shaders.Shader import androidx.compose.ui.graphics.RectangleShape
//import com.mikepenz.hypnoticcanvas.utils.round import androidx.compose.ui.graphics.Shape
// import androidx.compose.ui.layout.onGloballyPositioned
//@Composable import androidx.compose.ui.unit.Dp
//fun Modifier.shaderContent( import androidx.compose.ui.unit.dp
// shader: Shader, import com.mikepenz.hypnoticcanvas.NonAndroidRuntimeEffect
// speed: Float = 1f, import com.mikepenz.hypnoticcanvas.shaders.Shader
// fallback: () -> Brush = { import com.mikepenz.hypnoticcanvas.utils.round
// Brush.horizontalGradient(listOf(Color.Transparent, Color.Transparent))
// }, @Composable
//): Modifier { fun Modifier.shaderContent(
// val runtimeEffect = remember(shader) { NonAndroidRuntimeEffect(shader) } shader: Shader,
// var size: Size by remember { mutableStateOf(Size(-1f, -1f)) } speed: Float = 1f,
// val speedModifier = shader.speedModifier fallback: () -> Brush = {
// Brush.horizontalGradient(listOf(Color.Transparent, Color.Transparent))
// val time by if (runtimeEffect.supported) { },
// var startMillis = remember(shader) { -1L } ): Modifier {
// produceState(0f, speedModifier) { val runtimeEffect = remember(shader) { NonAndroidRuntimeEffect(shader) }
// while (true) { var size: Size by remember { mutableStateOf(Size(-1f, -1f)) }
// withInfiniteAnimationFrameMillis { val speedModifier = shader.speedModifier
// if (startMillis < 0) startMillis = it
// value = ((it - startMillis) / 16.6f) / 10f val time by if (runtimeEffect.supported) {
// } var startMillis = remember(shader) { -1L }
// } produceState(0f, speedModifier) {
// } while (true) {
// } else { withInfiniteAnimationFrameMillis {
// mutableStateOf(-1f) if (startMillis < 0) startMillis = it
// } value = ((it - startMillis) / 16.6f) / 10f
// }
// return this then Modifier }
// .onGloballyPositioned { }
// size = Size(it.size.width.toFloat(), it.size.height.toFloat()) } else {
// } mutableStateOf(-1f)
// .drawWithContent { }
// drawContent()
// // set uniforms for the shaders return this then Modifier
// runtimeEffect.update( .onGloballyPositioned {
// shader = shader, size = Size(it.size.width.toFloat(), it.size.height.toFloat())
// time = (time * speed * speedModifier).round(3), }
// width = size.width, .drawWithContent {
// height = size.height drawContent()
// ) // set uniforms for the shaders
// if (runtimeEffect.ready) { runtimeEffect.update(
// drawRect(brush = runtimeEffect.build(), blendMode = BlendMode.SrcAtop) shader = shader,
// } else { time = (time * speed * speedModifier).round(3),
// drawRect(brush = fallback()) width = size.width,
// } height = size.height
// } )
//} if (runtimeEffect.ready) {
drawRect(brush = runtimeEffect.build(), blendMode = BlendMode.SrcAtop)
} else {
drawRect(brush = fallback())
}
}
}
@Composable
fun Modifier.shaderBorder(
width: Dp = 1.dp,
shape: Shape = RectangleShape,
shader: Shader,
speed: Float = 1f,
fallback: () -> Brush = {
Brush.horizontalGradient(listOf(Color.Transparent, Color.Transparent))
},
): Modifier {
val runtimeEffect = remember(shader) { NonAndroidRuntimeEffect(shader) }
var size: Size by remember { mutableStateOf(Size(-1f, -1f)) }
val speedModifier = shader.speedModifier
val time by if (runtimeEffect.supported) {
var startMillis = remember(shader) { -1L }
produceState(0f, speedModifier) {
while (true) {
withInfiniteAnimationFrameMillis {
if (startMillis < 0) startMillis = it
value = ((it - startMillis) / 16.6f) / 10f
}
}
}
} else {
mutableStateOf(-1f)
}
return this then Modifier
.onGloballyPositioned {
size = Size(it.size.width.toFloat(), it.size.height.toFloat())
}
.border(
width = width,
shape = shape,
brush = run {
// set uniforms for the shaders
runtimeEffect.update(
shader = shader,
time = (time * speed * speedModifier).round(3),
width = size.width,
height = size.height
)
if (runtimeEffect.ready) {
runtimeEffect.build()
} else {
fallback()
}
},
)
}

View file

@ -0,0 +1,43 @@
package com.pixelized.desktop.lwa.ui.shaders.packs
import com.mikepenz.hypnoticcanvas.shaders.Shader
data object GoldenFlow : Shader {
override val name: String
get() = ""
override val authorName: String
get() = ""
override val authorUrl: String
get() = ""
override val credit: String
get() = ""
override val license: String
get() = ""
override val licenseUrl: String
get() = ""
override val sksl: String
get() = """
uniform float uTime;
uniform vec3 uResolution;
vec4 main(vec2 fragCoord) {
vec2 q = 7.0 * (fragCoord.xy - 0.5 * uResolution.xy) / max(uResolution.x, uResolution.y);
float i = 1.0;
for(int iter = 0; iter < 40; iter++)
{
vec2 o = q;
o.x += (0.5 / i) * cos(i * q.y + uTime * 0.297 + 0.03 * i) + 1.3;
o.y += (0.5 / i) * cos(i * q.x + uTime * 0.414 + 0.03 * (i + 10.0)) + 1.9;
q = o;
i *= 1.1;
}
vec3 col = vec3(0.5 * sin(3.0 * q.x) + 0.5, 0.5 * sin(3.0 * q.y) + 0.5, sin(1.3 * q.x + 1.7 * q.y));
float f = 0.43 * (col.x + col.y + col.z);
return vec4(f + 0.6, 0.2 + 0.75 * f, 0.2, 0.5);
}
""".trimIndent()
}

View file

@ -13,6 +13,7 @@ coil = "3.1.0"
zoomable = "2.7.0" zoomable = "2.7.0"
ui-graphics-android = "1.7.8" ui-graphics-android = "1.7.8"
buildkonfig = "0.17.0" buildkonfig = "0.17.0"
shader = "0.3.0"
[plugins] [plugins]
composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "compose-multiplatform" } composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "compose-multiplatform" }
@ -37,6 +38,9 @@ kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-
coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil" } coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil" }
coil-network-ktor = { module = "io.coil-kt.coil3:coil-network-ktor3", version.ref = "coil" } coil-network-ktor = { module = "io.coil-kt.coil3:coil-network-ktor3", version.ref = "coil" }
engawapg-zoomable = { module = "net.engawapg.lib:zoomable", version.ref = "zoomable" } engawapg-zoomable = { module = "net.engawapg.lib:zoomable", version.ref = "zoomable" }
# Shader
hypnoticcanvas = { module = "com.mikepenz.hypnoticcanvas:hypnoticcanvas", version.ref = "shader" }
hypnoticcanvas-shaders = { module = "com.mikepenz.hypnoticcanvas:hypnoticcanvas-shaders", version.ref = "shader" }
# Injection with Koin # Injection with Koin
koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" } koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" }
koin-ktor = { module = "io.insert-koin:koin-ktor", version.ref = "koin" } koin-ktor = { module = "io.insert-koin:koin-ktor", version.ref = "koin" }