Refactor the Character Ribbon (levelup)
This commit is contained in:
parent
24fe030663
commit
6364201c6c
14 changed files with 457 additions and 331 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
@ -84,27 +72,34 @@ 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(
|
||||
|
|
@ -161,5 +159,4 @@ fun CharacterSheetAlterationContent(
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
damagePercent = it,
|
||||
)
|
||||
|
||||
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}",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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}",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>>,
|
||||
)
|
||||
|
|
@ -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()
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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" }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue