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.
implementation(libs.coil.compose)
implementation(libs.coil.network.ktor)
// implementation("com.mikepenz.hypnoticcanvas:hypnoticcanvas:0.3.0")
// implementation("com.mikepenz.hypnoticcanvas:hypnoticcanvas-shaders:0.3.0")
// Shader
implementation(libs.hypnoticcanvas)
implementation(libs.hypnoticcanvas.shaders)
// network
implementation(libs.kotlinx.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.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import com.pixelized.desktop.lwa.LocalBlurController
import com.pixelized.desktop.lwa.ui.composable.blur.BlurContentController
import com.pixelized.desktop.lwa.ui.composable.decoratedBox.DecoratedBox
@ -32,6 +33,7 @@ object LwaDialogDefault {
@Composable
fun <T> LwaDialog(
modifier: Modifier = Modifier,
properties: DialogProperties = DialogProperties(),
blur: BlurContentController? = LocalBlurController.current,
paddings: PaddingValues = LwaDialogDefault.paddings,
color: Color = MaterialTheme.colors.surface,
@ -52,6 +54,7 @@ fun <T> LwaDialog(
}
Dialog(
properties = properties,
onDismissRequest = onDismissRequest,
content = {
Box(

View file

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

View file

@ -1,10 +1,16 @@
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.PointerMatcher
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.onClick
@ -13,13 +19,22 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.collectAsState
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.input.pointer.PointerButton
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.DpSize
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.CharacterRibbonPortrait
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.utils.extention.invert
@ -29,10 +44,11 @@ fun CharacterRibbon(
modifier: Modifier = Modifier,
layoutDirection: LayoutDirection,
viewModel: CharacterRibbonViewModel,
shape: Shape = MaterialTheme.lwa.shapes.portrait,
size: DpSize = MaterialTheme.lwa.dimen.portrait.minimized,
padding: PaddingValues = MaterialTheme.lwa.dimen.paddingValues,
onCharacterLeftClick: (characterSheetId: String) -> Unit,
onCharacterRightClick: (characterSheetId: String) -> Unit,
onLevelUp: (characterSheetId: String) -> Unit,
) {
val characters = viewModel.characters.collectAsState()
@ -53,6 +69,7 @@ fun CharacterRibbon(
Box(
modifier = Modifier
.animateItem()
.clip(shape = shape)
.graphicsLayer { if (it.hideOverruled) this.alpha = 0.3f }
.onClick(
matcher = PointerMatcher.mouse(PointerButton.Primary),
@ -66,19 +83,38 @@ fun CharacterRibbon(
),
) {
CharacterRibbonPortrait(
size = size,
character = it.portrait,
onLevelUp = { onLevelUp(it.characterSheetId) },
)
Column(
modifier = Modifier.size(size),
verticalArrangement = Arrangement.SpaceBetween,
) {
CompositionLocalProvider(
LocalLayoutDirection provides layoutDirection.invert
) {
CharacterRibbonAlteration(
alterations = it.alterations,
)
}
CharacterRibbonStats(
status = it.status,
)
}
CharacterRibbonRoll(
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.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.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.utils.extention.foldInto
import com.pixelized.shared.lwa.model.AlteredCharacterSheetFactory
@ -35,20 +37,25 @@ class CharacterRibbonFactory(
characterSheetId = characterSheet.id,
hideOverruled = hideOverruled,
enableDetail = enableCharacterSheet,
levelUp = alteredCharacterSheet.shouldLevelUp,
portrait = CharacterRibbonPortraitUio(
portrait = alteredCharacterSheet.thumbnail,
name = alteredCharacterSheet.name,
levelUp = alteredCharacterSheet.shouldLevelUp,
stats = takeIf { enableCharacterStats }?.let {
CharacterRibbonPortraitUio.StatsDetail(
damagePercent = takeIf { enableCharacterStats }?.let {
val maxHp = alteredCharacterSheet.maxHp.toFloat()
val damage = alteredCharacterSheet.damage.toFloat()
1f - ((maxHp - damage) / maxHp).coerceIn(0f, 1f)
},
),
status = takeIf { enableCharacterStats }?.let {
CharacterRibbonStatsUio(
hp = alteredCharacterSheet.maxHp - alteredCharacterSheet.damage,
maxHp = alteredCharacterSheet.maxHp,
pp = alteredCharacterSheet.maxPp - alteredCharacterSheet.fatigue,
maxPp = alteredCharacterSheet.maxPp,
)
},
),
status = takeIf { enableCharacterStatus }?.let {
alterations = takeIf { enableCharacterStatus }?.let {
alterations.map { alteration ->
CharacterRibbonAlterationUio(
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.graphics.Color
import com.pixelized.desktop.lwa.ui.theme.lwa
import kotlin.math.max
import kotlin.math.min
@Composable
fun BloodOverlay(
modifier: Modifier = Modifier,
bloodColor: Color = MaterialTheme.lwa.colorScheme.portrait.blood,
maxHp: Float,
hp: Float,
damagePercent: Float,
) {
val animatedRatio = animateFloatAsState(
targetValue = min(maxHp, max(0f, (maxHp - hp) / maxHp)),
targetValue = damagePercent,
animationSpec = tween(durationMillis = 350, easing = EaseOutCirc)
)
val animatedColor = animateColorAsState(
targetValue = bloodColor.copy(alpha = ((maxHp - hp) / maxHp) / 4f + .25f)
targetValue = bloodColor.copy(alpha = damagePercent / 4f + .25f)
)
Box(
modifier = modifier

View file

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

View file

@ -1,77 +1,54 @@
package com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.common
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
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.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.IconButton
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.draw.clip
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.Shape
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.Dp
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.shapes.ArrowShape
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 CharacterRibbonPortraitUio(
val portrait: String?,
val name: String,
val levelUp: Boolean,
val stats: StatsDetail?,
) {
@Stable
data class StatsDetail(
val hp: Int,
val maxHp: Int,
val pp: Int,
val maxPp: Int,
val damagePercent: Float?,
)
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun CharacterRibbonPortrait(
modifier: Modifier = Modifier,
background: Color = MaterialTheme.lwa.colorScheme.elevated.base1dp,
overlay: Brush = MaterialTheme.lwa.colorScheme.portraitBackgroundBrush,
size: DpSize = MaterialTheme.lwa.dimen.portrait.minimized,
levelUpOffset: Dp = 9.dp,
character: CharacterRibbonPortraitUio,
onLevelUp: () -> Unit,
) {
val colorScheme = MaterialTheme.lwa.colorScheme
Box(
modifier = modifier
.size(size = size)
.clip(shape = MaterialTheme.lwa.shapes.portrait)
.background(color = colorScheme.elevated.base1dp)
.background(color = background)
.drawWithContent {
drawContent()
drawRect(brush = overlay)
}
) {
AnimatedContent(
targetState = character.portrait,
@ -87,95 +64,10 @@ fun CharacterRibbonPortrait(
)
}
AnimatedVisibility(
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 ->
character.damagePercent?.let {
BloodOverlay(
maxHp = stats.maxHp.toFloat(),
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}",
damagePercent = it,
)
}
}
}
}
}

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 hideOverruled: Boolean,
val enableDetail: Boolean,
val levelUp: Boolean,
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
//
//import androidx.compose.animation.core.withInfiniteAnimationFrameMillis
//import androidx.compose.runtime.Composable
//import androidx.compose.runtime.getValue
//import androidx.compose.runtime.mutableStateOf
//import androidx.compose.runtime.produceState
//import androidx.compose.runtime.remember
//import androidx.compose.runtime.setValue
//import androidx.compose.ui.Modifier
//import androidx.compose.ui.draw.drawBehind
//import androidx.compose.ui.draw.drawWithContent
//import androidx.compose.ui.geometry.Size
//import androidx.compose.ui.graphics.BlendMode
//import androidx.compose.ui.graphics.Brush
//import androidx.compose.ui.graphics.Color
//import androidx.compose.ui.layout.onGloballyPositioned
//import com.mikepenz.hypnoticcanvas.NonAndroidRuntimeEffect
//import com.mikepenz.hypnoticcanvas.shaders.Shader
//import com.mikepenz.hypnoticcanvas.utils.round
//
//@Composable
//fun Modifier.shaderContent(
// 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())
// }
// .drawWithContent {
// drawContent()
// // set uniforms for the shaders
// runtimeEffect.update(
// shader = shader,
// time = (time * speed * speedModifier).round(3),
// width = size.width,
// height = size.height
// )
// if (runtimeEffect.ready) {
// drawRect(brush = runtimeEffect.build(), blendMode = BlendMode.SrcAtop)
// } else {
// drawRect(brush = fallback())
// }
// }
//}
package com.pixelized.desktop.lwa.ui.shaders
import androidx.compose.animation.core.withInfiniteAnimationFrameMillis
import androidx.compose.foundation.border
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.mikepenz.hypnoticcanvas.NonAndroidRuntimeEffect
import com.mikepenz.hypnoticcanvas.shaders.Shader
import com.mikepenz.hypnoticcanvas.utils.round
@Composable
fun Modifier.shaderContent(
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())
}
.drawWithContent {
drawContent()
// set uniforms for the shaders
runtimeEffect.update(
shader = shader,
time = (time * speed * speedModifier).round(3),
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"
ui-graphics-android = "1.7.8"
buildkonfig = "0.17.0"
shader = "0.3.0"
[plugins]
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-network-ktor = { module = "io.coil-kt.coil3:coil-network-ktor3", version.ref = "coil" }
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
koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" }
koin-ktor = { module = "io.insert-koin:koin-ktor", version.ref = "koin" }