Add a debug panel to the scene + MapScene + Cursor

This commit is contained in:
Thomas Andres Gomez 2025-10-24 19:04:08 +02:00
parent 4e013aa358
commit ae2c3da582
13 changed files with 858 additions and 153 deletions

View file

@ -2,15 +2,32 @@ package com.pixelized.desktop.lwa.ui.composable.scene
import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.VectorConverter import androidx.compose.animation.core.VectorConverter
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.widthIn
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastRoundToInt import androidx.compose.ui.util.fastRoundToInt
import com.pixelized.desktop.lwa.ui.composable.scene.drawables.SceneLayer
import com.pixelized.desktop.lwa.ui.theme.lwa
import com.pixelized.desktop.lwa.ui.theme.typography.LwaTypography
@Stable @Stable
class Camera( class Camera(
@ -98,4 +115,63 @@ class Camera(
else -> _zoom.animateTo(targetValue = initialZoom) else -> _zoom.animateTo(targetValue = initialZoom)
} }
} }
}
@Composable
fun SceneCameraDebug(
modifier: Modifier = Modifier,
camera: Camera,
isOpen: Boolean = true,
style: LwaTypography.Debug = MaterialTheme.lwa.typography.debug,
padding: Dp = MaterialTheme.lwa.dimen.debug.offset,
) {
val isOpen = remember { mutableStateOf(isOpen) }
Column(
modifier = Modifier
.clickable { isOpen.value = isOpen.value.not() }
.widthIn(min = MaterialTheme.lwa.dimen.debug.panelWidth)
.then(other = modifier),
verticalArrangement = Arrangement.spacedBy(space = 2.dp),
) {
Text(
style = MaterialTheme.lwa.typography.debug.title,
color = MaterialTheme.lwa.colorScheme.base.primary,
text = "Camera",
)
if (isOpen.value) {
Text(
modifier = Modifier.padding(start = padding),
style = MaterialTheme.lwa.typography.debug.propertyValue,
text = buildAnnotatedString {
withStyle(style.propertyId) { append("offset: ") }
append(camera.offset.toString())
},
)
Text(
modifier = Modifier.padding(start = padding),
style = MaterialTheme.lwa.typography.debug.propertyValue,
text = buildAnnotatedString {
withStyle(style.propertyId) { append("size: ") }
append(camera.cameraSize.toString())
},
)
Text(
modifier = Modifier.padding(start = padding),
style = MaterialTheme.lwa.typography.debug.propertyValue,
text = buildAnnotatedString {
withStyle(style.propertyId) { append("zoom: ") }
append(camera.zoom.toString())
},
)
Text(
modifier = Modifier.padding(start = padding),
style = MaterialTheme.lwa.typography.debug.propertyValue,
text = buildAnnotatedString {
withStyle(style.propertyId) { append("size: ") }
append(camera.cameraSizeZoomed.toString())
},
)
}
}
} }

View file

@ -0,0 +1,75 @@
package com.pixelized.desktop.lwa.ui.composable.scene
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.widthIn
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import com.pixelized.desktop.lwa.ui.theme.lwa
import com.pixelized.desktop.lwa.ui.theme.typography.LwaTypography
@Stable
class Cursor(
initial: IntOffset = IntOffset.Zero,
) {
private val _offset = mutableStateOf(initial)
val offset by _offset
fun change(
position: Offset,
) {
_offset.value = IntOffset(
x = position.x.toInt(),
y = position.y.toInt(),
)
}
}
@Composable
fun SceneCursorDebug(
modifier: Modifier = Modifier,
cursor: Cursor,
isOpen: Boolean = true,
style: LwaTypography.Debug = MaterialTheme.lwa.typography.debug,
padding: Dp = MaterialTheme.lwa.dimen.debug.offset,
) {
val isOpen = remember { mutableStateOf(isOpen) }
Column(
modifier = Modifier
.clickable { isOpen.value = isOpen.value.not() }
.widthIn(min = MaterialTheme.lwa.dimen.debug.panelWidth)
.then(other = modifier),
verticalArrangement = Arrangement.spacedBy(space = 2.dp),
) {
Text(
style = MaterialTheme.lwa.typography.debug.title,
color = MaterialTheme.lwa.colorScheme.base.primary,
text = "Cursor",
)
if (isOpen.value) {
Text(
modifier = Modifier.padding(start = padding),
style = MaterialTheme.lwa.typography.debug.propertyValue,
text = buildAnnotatedString {
withStyle(style.propertyId) { append("offset: ") }
append(cursor.offset.toString())
},
)
}
}
}

View file

@ -0,0 +1,154 @@
package com.pixelized.desktop.lwa.ui.composable.scene
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import com.pixelized.desktop.lwa.ui.screen.campaign.LocalCampaignLayoutScope
import kotlinx.coroutines.launch
import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.ic_center_focus_weak_24dp
import lwacharactersheet.composeapp.generated.resources.ic_visibility_24dp
import lwacharactersheet.composeapp.generated.resources.ic_zoom_in_map_24dp
import lwacharactersheet.composeapp.generated.resources.ic_zoom_out_map_24dp
import lwacharactersheet.composeapp.generated.resources.icon_d100
import lwacharactersheet.composeapp.generated.resources.image_dahome_maps
import lwacharactersheet.composeapp.generated.resources.image_dahome_regions
import org.jetbrains.compose.resources.painterResource
@Composable
fun MapScene(
modifier: Modifier = Modifier,
) {
val campaign = LocalCampaignLayoutScope.current
val scope = rememberCoroutineScope()
val map = rememberLayoutFromResource(
name = "Dahomé",
resource = Res.drawable.image_dahome_maps,
)
val mapRegionOverlay = rememberLayoutFromResource(
name = "Région",
resource = Res.drawable.image_dahome_regions,
)
val element1 = rememberElementFromResource(
name = "Group",
resource = Res.drawable.icon_d100,
offset = IntOffset(
x = 2128,
y = 1875,
),
)
val element2 = rememberElementFromResource(
name = "End",
resource = Res.drawable.icon_d100,
offset = IntOffset(
x = map.size.width,
y = map.size.height,
)
)
val element3 = rememberElementFromResource(
name = "Start",
resource = Res.drawable.icon_d100,
offset = IntOffset(
x = 0,
y = 0,
)
)
val scene = remember(map, mapRegionOverlay, element1, element2) {
Scene(
camera = Camera(
initialZoom = 1f,
initialOffset = IntOffset(x = -150, y = -120),
),
fogOfWar = FogOfWar.NONE,
layers = listOf(
map,
mapRegionOverlay,
),
elements = listOf(
element1,
element2,
element3,
),
)
}
Scene(
modifier = modifier,
scene = scene,
) {
Column(
modifier = Modifier
.align(alignment = Alignment.BottomEnd)
.padding(end = campaign.rightPanel.value.width)
.padding(all = 8.dp)
) {
IconButton(
onClick = {
scope.launch {
scene.camera.handleZoom(
zoomIn = true,
power = 0.3f,
)
}
}
) {
Icon(
painter = painterResource(Res.drawable.ic_zoom_in_map_24dp),
contentDescription = null
)
}
IconButton(
onClick = {
scope.launch {
scene.camera.handleZoom(
zoomIn = false,
power = 0.3f,
)
}
}
) {
Icon(
painter = painterResource(Res.drawable.ic_zoom_out_map_24dp),
contentDescription = null
)
}
IconButton(
onClick = {
scope.launch {
scene.camera.resetPosition()
}
scope.launch {
scene.camera.resetZoom()
}
}
) {
Icon(
painter = painterResource(Res.drawable.ic_center_focus_weak_24dp),
contentDescription = null
)
}
IconButton(
onClick = {
scope.launch {
scene.layers.getOrNull(1)?.let {
it.alpha(alpha = if (it.alpha == 0f) 1f else 0f)
}
}
}
) {
Icon(
painter = painterResource(Res.drawable.ic_visibility_24dp),
contentDescription = null
)
}
}
}
}

View file

@ -5,5 +5,9 @@ import androidx.compose.ui.graphics.Color
@Stable @Stable
data class FogOfWar( data class FogOfWar(
val color: Color = Color.Black.copy(alpha = 0.5f), val color: Color = Color.Black.copy(alpha = 0.0f),
) ) {
companion object {
val NONE = FogOfWar(color = Color.Transparent)
}
}

View file

@ -2,12 +2,9 @@ package com.pixelized.desktop.lwa.ui.composable.scene
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material.Icon import androidx.compose.material.MaterialTheme
import androidx.compose.material.IconButton
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@ -17,6 +14,9 @@ import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithCache import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.PointerEvent import androidx.compose.ui.input.pointer.PointerEvent
import androidx.compose.ui.input.pointer.PointerEventType import androidx.compose.ui.input.pointer.PointerEventType
@ -28,163 +28,107 @@ import androidx.compose.ui.input.pointer.onPointerEvent
import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.toSize
import com.pixelized.desktop.lwa.ui.screen.campaign.LocalCampaignLayoutScope import com.pixelized.desktop.lwa.ui.composable.scene.drawables.SceneElement
import com.pixelized.desktop.lwa.ui.composable.scene.drawables.SceneLayer
import com.pixelized.desktop.lwa.ui.composable.scene.utils.global
import com.pixelized.desktop.lwa.ui.composable.scene.utils.local
import com.pixelized.desktop.lwa.ui.theme.lwa
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.ic_center_focus_weak_24dp
import lwacharactersheet.composeapp.generated.resources.ic_zoom_in_map_24dp
import lwacharactersheet.composeapp.generated.resources.ic_zoom_out_map_24dp
import lwacharactersheet.composeapp.generated.resources.ic_visibility_24dp
import lwacharactersheet.composeapp.generated.resources.icon_d100
import lwacharactersheet.composeapp.generated.resources.image_dahome_maps
import lwacharactersheet.composeapp.generated.resources.image_dahome_regions
import org.jetbrains.compose.resources.DrawableResource import org.jetbrains.compose.resources.DrawableResource
import org.jetbrains.compose.resources.imageResource import org.jetbrains.compose.resources.imageResource
import org.jetbrains.compose.resources.painterResource import java.util.UUID
import kotlin.math.sign import kotlin.math.sign
@Stable @Stable
data class Scene( data class Scene(
val camera: Camera, val camera: Camera,
val layouts: List<Layout>,
val fogOfWar: FogOfWar, val fogOfWar: FogOfWar,
val layers: List<SceneLayer>,
val elements: List<SceneElement>,
) { ) {
val size: IntSize = IntSize( val size: IntSize = IntSize(
width = layouts.maxOf { it.size.width }, width = layers.maxOf { it.size.width },
height = layouts.maxOf { it.size.height }, height = layers.maxOf { it.size.height },
) )
} }
@Composable @Composable
fun Scene( fun Scene(
modifier: Modifier, modifier: Modifier = Modifier,
scene: Scene,
content: @Composable BoxScope.() -> Unit,
) { ) {
val campaign = LocalCampaignLayoutScope.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val scene = rememberScene( val cursors = remember {
camera = Camera( listOf(
initialZoom = 1f, Cursor()
initialOffset = IntOffset(x = -150, y = -120), )
), }
fogOfWar = FogOfWar(),
rememberLayoutFromResource(
resource = Res.drawable.image_dahome_maps,
),
rememberLayoutFromResource(
resource = Res.drawable.image_dahome_regions,
),
rememberLayoutFromResource(
resource = Res.drawable.icon_d100,
offset = IntOffset(x = 1740, y = 910),
),
)
Box( Box(
modifier = modifier modifier = modifier
.graphicsLayer { clip = true } .graphicsLayer { clip = true }
.onCameraControl(scope = scope, scene = scene) .onCameraControl(scope = scope, scene = scene)
.drawScene(scene = scene) .onCursorControl(camera = scene.camera, cursor = cursors.first())
.fogOfWar(scene = scene) .drawLayers(camera = scene.camera, layers = scene.layers)
.drawElements(camera = scene.camera, elements = scene.elements)
.drawCursors(camera = scene.camera, cursors = cursors)
.drawFogOfWar(scene = scene),
) { ) {
Column( content()
modifier = Modifier
.align(alignment = Alignment.BottomEnd)
.padding(end = campaign.rightPanel.value.width)
.padding(all = 8.dp)
) {
IconButton(
onClick = {
scope.launch {
scene.camera.handleZoom(
zoomIn = true,
power = 0.3f,
)
}
}
) {
Icon(
painter = painterResource(Res.drawable.ic_zoom_in_map_24dp),
contentDescription = null
)
}
IconButton(
onClick = {
scope.launch {
scene.camera.handleZoom(
zoomIn = false,
power = 0.3f,
)
}
}
) {
Icon(
painter = painterResource(Res.drawable.ic_zoom_out_map_24dp),
contentDescription = null
)
}
IconButton(
onClick = {
scope.launch {
scene.camera.resetPosition()
}
scope.launch {
scene.camera.resetZoom()
}
}
) {
Icon(
painter = painterResource(Res.drawable.ic_center_focus_weak_24dp),
contentDescription = null
)
}
IconButton(
onClick = {
scope.launch {
scene.layouts.getOrNull(1)?.let {
it.alpha(alpha = if (it.alpha == 0f) 1f else 0f)
}
}
}
) {
Icon(
painter = painterResource(Res.drawable.ic_visibility_24dp),
contentDescription = null
)
}
}
}
}
@Composable SceneDebugPanel(
@Stable modifier = Modifier
fun rememberLayoutFromResource( .align(alignment = Alignment.TopEnd)
resource: DrawableResource, .padding(paddingValues = MaterialTheme.lwa.dimen.paddingValues),
offset: IntOffset = IntOffset.Zero, cursor = cursors.first(),
): Layout { scene = scene,
val texture = imageResource(
resource = resource,
)
return remember(resource) {
Layout(
texture = texture,
offset = offset,
) )
} }
} }
@Composable @Composable
@Stable @Stable
fun rememberScene( fun rememberLayoutFromResource(
camera: Camera, name: String,
fogOfWar: FogOfWar, resource: DrawableResource,
vararg layouts: Layout, offset: IntOffset = IntOffset.Zero,
): Scene { ): SceneLayer {
return remember { val texture = imageResource(
Scene( resource = resource,
camera = camera, )
layouts = layouts.toList(), return remember(resource) {
fogOfWar = fogOfWar, SceneLayer(
id = UUID.randomUUID().toString(),
name = name,
texture = texture,
offset = offset,
alpha = 1f,
)
}
}
@Composable
@Stable
fun rememberElementFromResource(
name: String,
resource: DrawableResource,
offset: IntOffset = IntOffset.Zero,
): SceneElement {
val texture = imageResource(
resource = resource,
)
return remember(resource) {
SceneElement(
id = UUID.randomUUID().toString(),
name = name,
texture = texture,
offset = offset,
alpha = 1f,
) )
} }
} }
@ -233,23 +177,81 @@ fun Modifier.onCameraControl(
} }
} }
fun Modifier.drawScene( @OptIn(ExperimentalComposeUiApi::class)
scene: Scene, fun Modifier.onCursorControl(
camera: Camera,
cursor: Cursor,
): Modifier = this
.onPointerEvent(PointerEventType.Exit) { event: PointerEvent ->
cursor.change(
position = Offset.Unspecified,
)
}
.onPointerEvent(PointerEventType.Move) { event: PointerEvent ->
cursor.change(
position = event.changes.first().position.global(camera = camera),
)
}
private fun Modifier.drawCursors(
camera: Camera,
cursors: List<Cursor>,
): Modifier = this
.drawWithCache {
onDrawBehind {
cursors.forEach { cursor ->
drawRect(
color = Color.Green,
topLeft = cursor.offset.local(camera = camera),
size = Size(10f, 10f),
style = Stroke(width = 2f),
)
}
}
}
@OptIn(ExperimentalComposeUiApi::class)
fun Modifier.drawLayers(
camera: Camera,
layers: List<SceneLayer>,
): Modifier = this.drawWithCache { ): Modifier = this.drawWithCache {
onDrawBehind { onDrawBehind {
scene.layouts.forEach { layout -> layers.forEach { layers ->
drawImage( drawImage(
image = layout.texture, image = layers.texture,
srcOffset = scene.camera.offset - layout.offset, srcOffset = camera.offset - layers.offset,
srcSize = scene.camera.cameraSizeZoomed, srcSize = camera.cameraSizeZoomed,
dstSize = scene.camera.cameraSize, dstSize = camera.cameraSize,
alpha = layout.alpha, alpha = layers.alpha,
) )
} }
} }
} }
fun Modifier.fogOfWar( fun Modifier.drawElements(
camera: Camera,
elements: List<SceneElement>,
): Modifier = this.drawWithCache {
onDrawBehind {
elements.forEach { element ->
drawImage(
image = element.texture,
srcOffset = camera.offset - element.position,
srcSize = camera.cameraSizeZoomed,
dstSize = camera.cameraSize,
alpha = element.alpha,
)
drawRect(
color = Color.Red,
topLeft = element.position.local(camera = camera),
size = (element.size).toSize() / camera.zoom,
style = Stroke(width = 2f),
)
}
}
}
fun Modifier.drawFogOfWar(
scene: Scene, scene: Scene,
): Modifier = this.drawWithCache { ): Modifier = this.drawWithCache {
onDrawBehind { onDrawBehind {

View file

@ -0,0 +1,114 @@
package com.pixelized.desktop.lwa.ui.composable.scene
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.widthIn
import androidx.compose.material.Card
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.pixelized.desktop.lwa.ui.composable.scene.drawables.SceneElementDebug
import com.pixelized.desktop.lwa.ui.composable.scene.drawables.SceneLayerDebug
import com.pixelized.desktop.lwa.ui.theme.lwa
import com.pixelized.desktop.lwa.ui.theme.typography.LwaTypography
@Composable
fun SceneDebugPanel(
modifier: Modifier = Modifier,
paddingValues: PaddingValues = MaterialTheme.lwa.dimen.paddingValues,
cursor: Cursor,
scene: Scene,
) {
Card (
modifier = modifier,
backgroundColor = MaterialTheme.lwa.colorScheme.elevated.base4dp,
) {
Column(
modifier = Modifier.padding(paddingValues = paddingValues),
verticalArrangement = Arrangement.spacedBy(space = 8.dp),
) {
SceneDebug(
scene = scene,
)
SceneCursorDebug(
cursor = cursor,
)
SceneCameraDebug(
camera = scene.camera,
)
Column {
Text(
style = MaterialTheme.lwa.typography.debug.title,
text = "Layers:(${scene.layers.size})"
)
scene.layers.forEach { layer ->
SceneLayerDebug(
modifier = Modifier.padding(start = MaterialTheme.lwa.dimen.debug.offset),
isOpen = false,
layer = layer,
)
}
}
Column {
Text(
style = MaterialTheme.lwa.typography.debug.title,
text = "Elements:(${scene.elements.size})"
)
scene.elements.forEach { element ->
SceneElementDebug(
modifier = Modifier.padding(start = MaterialTheme.lwa.dimen.debug.offset),
isOpen = false,
element = element,
)
}
}
}
}
}
@Composable
private fun SceneDebug(
modifier: Modifier = Modifier,
scene: Scene,
isOpen: Boolean = true,
style: LwaTypography.Debug = MaterialTheme.lwa.typography.debug,
padding: Dp = MaterialTheme.lwa.dimen.debug.offset,
) {
val isOpen = remember { mutableStateOf(isOpen) }
Column(
modifier = Modifier
.clickable { isOpen.value = isOpen.value.not() }
.widthIn(min = MaterialTheme.lwa.dimen.debug.panelWidth)
.then(other = modifier),
verticalArrangement = Arrangement.spacedBy(space = 2.dp),
) {
Text(
style = MaterialTheme.lwa.typography.debug.title,
color = MaterialTheme.lwa.colorScheme.base.primary,
text = "Scene",
)
if (isOpen.value) {
Text(
modifier = Modifier.padding(start = padding),
style = MaterialTheme.lwa.typography.debug.propertyValue,
text = buildAnnotatedString {
withStyle(style.propertyId) { append("Size: ") }
append(scene.size.toString())
},
)
}
}
}

View file

@ -1,22 +1,22 @@
package com.pixelized.desktop.lwa.ui.composable.scene package com.pixelized.desktop.lwa.ui.composable.scene.drawables
import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.VectorConverter import androidx.compose.animation.core.VectorConverter
import androidx.compose.runtime.Stable
import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.IntSize
@Stable open class SceneDrawable(
class Layout( val id: String,
val name: String,
val texture: ImageBitmap, val texture: ImageBitmap,
val offset: IntOffset = IntOffset.Zero, val offset: IntOffset,
val size: IntSize = IntSize(texture.width, texture.height), val size: IntSize,
private val initialAlpha: Float = 1f, private val initialAlpha: Float,
) { ) {
private val _alpha = Animatable( private val _alpha = Animatable(
initialValue = initialAlpha, initialValue = initialAlpha,
typeConverter = Float.VectorConverter, typeConverter = Float.Companion.VectorConverter,
) )
val alpha get() = _alpha.value val alpha get() = _alpha.value

View file

@ -0,0 +1,104 @@
package com.pixelized.desktop.lwa.ui.composable.scene.drawables
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.widthIn
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import com.pixelized.desktop.lwa.ui.theme.lwa
import com.pixelized.desktop.lwa.ui.theme.typography.LwaTypography
@Stable
class SceneElement(
id: String,
name: String,
texture: ImageBitmap,
offset: IntOffset = IntOffset.Companion.Zero,
size: IntSize = IntSize(texture.width, texture.height),
alpha: Float = 1f,
) : SceneDrawable(
id = id,
name = name,
texture = texture,
offset = offset,
size = size,
initialAlpha = alpha,
) {
val position = IntOffset(
x = offset.x - size.width / 2,
y = offset.y - size.height / 2,
)
}
@Composable
fun SceneElementDebug(
modifier: Modifier = Modifier,
element: SceneElement,
isOpen: Boolean = true,
style: LwaTypography.Debug = MaterialTheme.lwa.typography.debug,
padding: Dp = MaterialTheme.lwa.dimen.debug.offset,
) {
val isOpen = remember { mutableStateOf(isOpen) }
Column(
modifier = Modifier
.clickable { isOpen.value = isOpen.value.not() }
.widthIn(min = MaterialTheme.lwa.dimen.debug.panelWidth)
.then(other = modifier),
verticalArrangement = Arrangement.spacedBy(space = 2.dp),
) {
Text(
style = MaterialTheme.lwa.typography.debug.title,
color = MaterialTheme.lwa.colorScheme.base.primary,
text = element.name,
)
if (isOpen.value) {
Text(
modifier = Modifier.padding(start = padding),
style = MaterialTheme.lwa.typography.debug.propertyValue,
text = buildAnnotatedString {
withStyle(style.propertyId) { append("id: ") }
append(element.id)
},
)
Text(
modifier = Modifier.padding(start = padding),
style = MaterialTheme.lwa.typography.debug.propertyValue,
text = buildAnnotatedString {
withStyle(style.propertyId) { append("offset: ") }
append(element.offset.toString())
},
)
Text(
modifier = Modifier.padding(start = padding),
style = MaterialTheme.lwa.typography.debug.propertyValue,
text = buildAnnotatedString {
withStyle(style.propertyId) { append("size: ") }
append(element.size.toString())
},
)
Text(
modifier = Modifier.padding(start = padding),
style = MaterialTheme.lwa.typography.debug.propertyValue,
text = buildAnnotatedString {
withStyle(style.propertyId) { append("alpha: ") }
append(element.alpha.toString())
},
)
}
}
}

View file

@ -0,0 +1,99 @@
package com.pixelized.desktop.lwa.ui.composable.scene.drawables
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.widthIn
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import com.pixelized.desktop.lwa.ui.theme.lwa
import com.pixelized.desktop.lwa.ui.theme.typography.LwaTypography
@Stable
class SceneLayer(
id : String,
name: String,
texture: ImageBitmap,
offset: IntOffset = IntOffset.Companion.Zero,
size: IntSize = IntSize(texture.width, texture.height),
alpha: Float = 1f,
) : SceneDrawable(
id = id,
name = name,
texture = texture,
offset = offset,
size = size,
initialAlpha = alpha,
)
@Composable
fun SceneLayerDebug(
modifier: Modifier = Modifier,
layer: SceneLayer,
isOpen: Boolean = true,
style: LwaTypography.Debug = MaterialTheme.lwa.typography.debug,
padding: Dp = MaterialTheme.lwa.dimen.debug.offset,
) {
val isOpen = remember { mutableStateOf(isOpen) }
Column(
modifier = Modifier
.clickable { isOpen.value = isOpen.value.not() }
.widthIn(min = MaterialTheme.lwa.dimen.debug.panelWidth)
.then(other = modifier),
verticalArrangement = Arrangement.spacedBy(space = 2.dp),
) {
Text(
style = MaterialTheme.lwa.typography.debug.title,
color = MaterialTheme.lwa.colorScheme.base.primary,
text = layer.name,
)
if (isOpen.value) {
Text(
modifier = Modifier.padding(start = padding),
style = MaterialTheme.lwa.typography.debug.propertyValue,
text = buildAnnotatedString {
withStyle(style.propertyId) { append("id: ") }
append(layer.id)
},
)
Text(
modifier = Modifier.padding(start = padding),
style = MaterialTheme.lwa.typography.debug.propertyValue,
text = buildAnnotatedString {
withStyle(style.propertyId) { append("offset: ") }
append(layer.offset.toString())
},
)
Text(
modifier = Modifier.padding(start = padding),
style = MaterialTheme.lwa.typography.debug.propertyValue,
text = buildAnnotatedString {
withStyle(style.propertyId) { append("size: ") }
append(layer.size.toString())
},
)
Text(
modifier = Modifier.padding(start = padding),
style = MaterialTheme.lwa.typography.debug.propertyValue,
text = buildAnnotatedString {
withStyle(style.propertyId) { append("alpha: ") }
append(layer.alpha.toString())
},
)
}
}
}

View file

@ -0,0 +1,39 @@
package com.pixelized.desktop.lwa.ui.composable.scene.utils
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.unit.IntOffset
import com.pixelized.desktop.lwa.ui.composable.scene.Camera
/**
* Convert local positon to global one.
* Global position are agnostic from camera, and therefor should be use to position stuff on the map.
* A common use case is to share players cursor.
*/
fun Offset.global(
camera: Camera,
): Offset = Offset(
x = this.x * camera.zoom + camera.offset.x,
y = this.y * camera.zoom + camera.offset.y,
)
/**
* Convert global positon to local one.
* Local position take into account the camera and are use to display stuff on the Scene composable.
*/
fun Offset.local(
camera: Camera,
): Offset = Offset(
x = (this.x - camera.offset.x) / camera.zoom,
y = (this.y - camera.offset.y) / camera.zoom,
)
/**
* Convert global positon to local one.
* Local position take into account the camera and are use to display stuff on the Scene composable.
*/
fun IntOffset.local(
camera: Camera,
): Offset = Offset(
x = (this.x.toFloat() - camera.offset.x) / camera.zoom,
y = (this.y.toFloat() - camera.offset.y) / camera.zoom,
)

View file

@ -44,7 +44,7 @@ import com.pixelized.desktop.lwa.ui.composable.character.diminished.CharacterShe
import com.pixelized.desktop.lwa.ui.composable.character.diminished.CharacterSheetDiminishedDialogViewModel import com.pixelized.desktop.lwa.ui.composable.character.diminished.CharacterSheetDiminishedDialogViewModel
import com.pixelized.desktop.lwa.ui.composable.error.ErrorSnackHandler import com.pixelized.desktop.lwa.ui.composable.error.ErrorSnackHandler
import com.pixelized.desktop.lwa.ui.composable.key.KeyHandler import com.pixelized.desktop.lwa.ui.composable.key.KeyHandler
import com.pixelized.desktop.lwa.ui.composable.scene.Scene import com.pixelized.desktop.lwa.ui.composable.scene.MapScene
import com.pixelized.desktop.lwa.ui.navigation.screen.LocalScreenController import com.pixelized.desktop.lwa.ui.navigation.screen.LocalScreenController
import com.pixelized.desktop.lwa.ui.navigation.screen.destination.navigateToLevelScreen import com.pixelized.desktop.lwa.ui.navigation.screen.destination.navigateToLevelScreen
import com.pixelized.desktop.lwa.ui.overlay.portrait.PortraitOverlay import com.pixelized.desktop.lwa.ui.overlay.portrait.PortraitOverlay
@ -101,7 +101,7 @@ fun CampaignScreen(
}, },
main = { main = {
Scene( MapScene(
modifier = Modifier.matchParentSize(), modifier = Modifier.matchParentSize(),
) )
}, },

View file

@ -15,6 +15,7 @@ data class LwaDimen(
val layout: Layout, val layout: Layout,
val portrait: Portrait, val portrait: Portrait,
val sheet: Sheet, val sheet: Sheet,
val debug: Debug,
) { ) {
@Stable @Stable
data class Layout( data class Layout(
@ -33,6 +34,12 @@ data class LwaDimen(
val subCategory: Dp, val subCategory: Dp,
val characteristic: DpSize, val characteristic: DpSize,
) )
@Stable
data class Debug(
val panelWidth: Dp,
val offset: Dp,
)
} }
@Composable @Composable
@ -52,6 +59,10 @@ fun lwaDimen(
detailWidth = 128.dp * 4, detailWidth = 128.dp * 4,
chatMaxWidth = 600.dp, chatMaxWidth = 600.dp,
), ),
debug: LwaDimen.Debug = LwaDimen.Debug(
panelWidth = 200.dp,
offset = 8.dp,
)
): LwaDimen { ): LwaDimen {
return remember { return remember {
LwaDimen( LwaDimen(
@ -60,6 +71,7 @@ fun lwaDimen(
portrait = portrait, portrait = portrait,
sheet = sheet, sheet = sheet,
layout = layout, layout = layout,
debug = debug,
) )
} }
} }

View file

@ -10,6 +10,7 @@ import androidx.compose.ui.graphics.Shadow
import androidx.compose.ui.graphics.StrokeJoin import androidx.compose.ui.graphics.StrokeJoin
import androidx.compose.ui.graphics.drawscope.Fill import androidx.compose.ui.graphics.drawscope.Fill
import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontStyle
@ -30,6 +31,7 @@ data class LwaTypography(
val characterSheet: CharacterSheet, val characterSheet: CharacterSheet,
val inventory: Inventory, val inventory: Inventory,
val freeDiceThrow: Dice, val freeDiceThrow: Dice,
val debug: Debug,
) { ) {
@Stable @Stable
data class Chat( data class Chat(
@ -73,6 +75,13 @@ data class LwaTypography(
val dice: TextStyle, val dice: TextStyle,
val result: TextStyle, val result: TextStyle,
) )
@Stable
data class Debug(
val title: TextStyle,
val propertyId: SpanStyle,
val propertyValue: TextStyle,
)
} }
@Composable @Composable
@ -196,7 +205,24 @@ fun lwaTypography(
result = robotoMono.h4.copy( result = robotoMono.h4.copy(
color = colors.base.onSurface, color = colors.base.onSurface,
), ),
) ),
debug = LwaTypography.Debug(
title = robotoMono.caption.copy(
fontWeight = FontWeight.Medium,
fontSize = 14.sp,
lineHeight = 14.sp,
),
propertyId = robotoMono.caption.copy(
fontWeight = FontWeight.Normal,
fontSize = 10.sp,
lineHeight = 14.sp,
).toSpanStyle(),
propertyValue = robotoMono.caption.copy(
fontWeight = FontWeight.Normal,
fontSize = 12.sp,
lineHeight = 14.sp,
),
),
) )
} }
} }