From 3485b8a9fd9c34505d96c0acca47b1bfb3417191 Mon Sep 17 00:00:00 2001 From: Thomas Andres Gomez Date: Sat, 6 Dec 2025 14:24:37 +0100 Subject: [PATCH] Refactor the camera scene (camera) system. --- .../desktop/lwa/ui/composable/scene/Scene.kt | 61 +--------- .../lwa/ui/composable/scene/camera/Camera.kt | 78 ++++++------- .../scene/camera/Modifier+onCameraControl.kt | 36 +++--- .../scene/debug/CursorDebugPanel.kt | 13 ++- .../scene/debug/ElementDebugPanel.kt | 79 +++++++++++++ .../composable/scene/debug/LayerDebugPanel.kt | 79 +++++++++++++ .../composable/scene/debug/SceneDebugPanel.kt | 6 +- .../scene/drawables/SceneElement.kt | 90 ++++----------- .../composable/scene/drawables/SceneLayer.kt | 92 ++++------------ .../ui/composable/scene/fogOfWar/FogOfWar.kt | 8 +- .../scene/fogOfWar/Modifier+drawFogOfWar.kt | 2 +- .../scene/utils/IntOffset+Coordinate.kt | 104 ++++++++++++++++++ .../scene/utils/Offset+Coordinate.kt | 81 ++++++++++++-- .../lwa/ui/screen/campaign/map/DahomeMap.kt | 22 ++-- 14 files changed, 453 insertions(+), 298 deletions(-) create mode 100644 composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/debug/ElementDebugPanel.kt create mode 100644 composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/debug/LayerDebugPanel.kt create mode 100644 composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/utils/IntOffset+Coordinate.kt diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/Scene.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/Scene.kt index dace034..841ab95 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/Scene.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/Scene.kt @@ -4,25 +4,16 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxScope import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable -import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.draw.drawWithCache -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntSize -import androidx.compose.ui.unit.toSize import com.pixelized.desktop.lwa.ui.composable.scene.camera.Camera import com.pixelized.desktop.lwa.ui.composable.scene.camera.onCameraControl 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.local -import org.jetbrains.compose.resources.DrawableResource -import org.jetbrains.compose.resources.imageResource -import java.util.UUID @Stable data class Scene( @@ -43,12 +34,12 @@ fun Scene( scene: Scene, content: @Composable BoxScope.() -> Unit, ) { - val scope = rememberCoroutineScope() + rememberCoroutineScope() Box( modifier = Modifier .graphicsLayer { clip = true } - .onCameraControl(scope = scope, sceneSize = scene.size, camera = camera) + .onCameraControl(sceneSize = scene.size, camera = camera) .drawLayers(camera = camera, layers = scene.layers) .drawElements(camera = camera, elements = scene.elements) .then(other = modifier), @@ -57,48 +48,6 @@ fun Scene( } } -@Composable -@Stable -fun rememberLayoutFromResource( - name: String, - resource: DrawableResource, - offset: IntOffset = IntOffset.Zero, -): SceneLayer { - val texture = imageResource( - resource = resource, - ) - return remember(resource) { - 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, - ) - } -} - @OptIn(ExperimentalComposeUiApi::class) fun Modifier.drawLayers( camera: Camera, @@ -130,12 +79,6 @@ fun Modifier.drawElements( 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), - ) } } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/camera/Camera.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/camera/Camera.kt index cd48fab..c52ddfb 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/camera/Camera.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/camera/Camera.kt @@ -1,13 +1,12 @@ package com.pixelized.desktop.lwa.ui.composable.scene.camera -import androidx.compose.animation.core.Animatable -import androidx.compose.animation.core.VectorConverter import androidx.compose.runtime.Stable import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntSize import androidx.compose.ui.util.fastRoundToInt @@ -17,22 +16,11 @@ class Camera( private val initialZoom: Float = 2f, private val initialOffset: IntOffset = IntOffset.Zero, ) { - private var _zoom = Animatable( - initialValue = initialZoom, - typeConverter = Float.VectorConverter, - ) - val zoom: Float get() = _zoom.value + private var _zoom = mutableStateOf(initialZoom) + val zoom: Float by _zoom - private var _offset = Animatable( - initialValue = initialOffset, - typeConverter = IntOffset.VectorConverter, - ) - val offset: IntOffset by derivedStateOf { - _offset.value + IntOffset( - x = (_sceneSize.width - cameraSizeZoomed.width) / 2, - y = (_sceneSize.height - cameraSizeZoomed.height) / 2, - ) - } + private var _offset = mutableStateOf(initialOffset) + val offset: IntOffset by _offset private var _sceneSize: IntSize by mutableStateOf(IntSize.Zero) private var _cameraSize: IntSize by mutableStateOf(IntSize.Zero) @@ -52,46 +40,48 @@ class Camera( _sceneSize = sceneSize } - suspend fun handlePanning( + fun handlePanning( delta: Offset, - snap: Boolean, ) { val value = _offset.value - IntOffset( x = (delta.x * zoom).fastRoundToInt(), y = (delta.y * zoom).fastRoundToInt(), ) - when { - snap -> _offset.snapTo(targetValue = value) - else -> _offset.animateTo(targetValue = value) - } + _offset.value = value } - suspend fun handleZoom( + fun handleZoom( power: Float, - snap: Boolean = false, + target: IntOffset? = null, ) { - val value = _zoom.value * (1f - power) - when { - snap -> _zoom.snapTo(targetValue = value) - else -> _zoom.animateTo(targetValue = value) - } + val zoomTarget = _zoom.value * (1f - power) + + val projection = cameraSizeProjection( + zoomTarget = zoomTarget, + ) + val targetDelta = Offset( + x = target?.x?.toFloat()?.div(_cameraSize.width.toFloat()) ?: 0.5f, + y = target?.y?.toFloat()?.div(_cameraSize.height.toFloat()) ?: 0.5f, + ) + val offsetTarget = _offset.value + IntOffset( + x = (targetDelta.x * projection.width).fastRoundToInt(), + y = (targetDelta.y * projection.height).fastRoundToInt(), + ) + + _zoom.value = zoomTarget + _offset.value = offsetTarget } - suspend fun resetPosition( - snap: Boolean = false, - ) { - when (snap) { - true -> _offset.snapTo(targetValue = initialOffset) - else -> _offset.animateTo(targetValue = initialOffset) - } + fun resetPosition() { + _offset.value = initialOffset } - suspend fun resetZoom( - snap: Boolean = false, - ) { - when (snap) { - true -> _zoom.snapTo(targetValue = initialZoom) - else -> _zoom.animateTo(targetValue = initialZoom) - } + fun resetZoom() { + _zoom.value = initialZoom } + + private fun cameraSizeProjection(zoomTarget: Float): Size = Size( + width = cameraSize.width * _zoom.value - cameraSize.width * zoomTarget, + height = cameraSize.height * _zoom.value - cameraSize.height * zoomTarget, + ) } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/camera/Modifier+onCameraControl.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/camera/Modifier+onCameraControl.kt index ddd3218..e3e6034 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/camera/Modifier+onCameraControl.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/camera/Modifier+onCameraControl.kt @@ -12,13 +12,11 @@ import androidx.compose.ui.input.pointer.isTertiaryPressed import androidx.compose.ui.input.pointer.onPointerEvent import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.unit.IntSize -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch +import androidx.compose.ui.unit.round import kotlin.math.sign @OptIn(ExperimentalFoundationApi::class, ExperimentalComposeUiApi::class) fun Modifier.onCameraControl( - scope: CoroutineScope, sceneSize: IntSize, camera: Camera, ): Modifier { @@ -31,26 +29,22 @@ fun Modifier.onCameraControl( ) } .onPointerEvent(PointerEventType.Move) { event: PointerEvent -> - scope.launch { - offsetDelta.handlePositionChange( - event = event, - ) { delta -> - when { - event.buttons.isTertiaryPressed || (event.keyboardModifiers.isCtrlPressed && event.buttons.isPrimaryPressed) -> camera.handlePanning( - delta = delta, - snap = true, - ) - } + offsetDelta.handlePositionChange( + event = event, + ) { delta -> + when { + event.buttons.isTertiaryPressed || (event.keyboardModifiers.isCtrlPressed && event.buttons.isPrimaryPressed) -> camera.handlePanning( + delta = delta, + ) } } } .onPointerEvent(PointerEventType.Scroll) { event: PointerEvent -> - scope.launch { - camera.handleZoom( - power = -event.changes.first().scrollDelta.y.sign * 0.15f, - snap = false, - ) - } + val change = event.changes.first() + camera.handleZoom( + power = -change.scrollDelta.y.sign * 0.15f, + target = change.position.round(), + ) } } @@ -59,10 +53,10 @@ private data class CursorDelta( var previousPosition: Offset = Offset.Zero, var currentPosition: Offset = Offset.Zero, ) { - suspend inline fun handlePositionChange( + inline fun handlePositionChange( event: PointerEvent, delay: Float = 10f, - crossinline block: suspend (delta: Offset) -> Unit, + crossinline block: (delta: Offset) -> Unit, ) { val currentTimestamp = System.currentTimeMillis() if (currentTimestamp - lastDeltaTimestamp > delay) { diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/debug/CursorDebugPanel.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/debug/CursorDebugPanel.kt index bdc5fb0..8a9329d 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/debug/CursorDebugPanel.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/debug/CursorDebugPanel.kt @@ -16,7 +16,9 @@ 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.camera.Camera import com.pixelized.desktop.lwa.ui.composable.scene.cursor.Cursor +import com.pixelized.desktop.lwa.ui.composable.scene.utils.local import com.pixelized.desktop.lwa.ui.theme.lwa import com.pixelized.desktop.lwa.ui.theme.typography.LwaTypography @@ -26,6 +28,7 @@ fun CursorDebugPanel( style: LwaTypography.Debug = MaterialTheme.lwa.typography.debug, padding: Dp = MaterialTheme.lwa.dimen.debug.offset, spacing: Dp = 2.dp, + camera: Camera, cursors: List, isOpen: Boolean = true, ) { @@ -50,10 +53,18 @@ fun CursorDebugPanel( modifier = Modifier.padding(start = padding), style = MaterialTheme.lwa.typography.debug.propertyValue, text = buildAnnotatedString { - withStyle(style.propertyIdSpan) { append("coordinate: ") } + withStyle(style.propertyIdSpan) { append("global: ") } append(cursor.offset.toString()) }, ) + Text( + modifier = Modifier.padding(start = padding), + style = MaterialTheme.lwa.typography.debug.propertyValue, + text = buildAnnotatedString { + withStyle(style.propertyIdSpan) { append("local: ") } + append(cursor.offset.local(camera).toString()) + }, + ) } } } diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/debug/ElementDebugPanel.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/debug/ElementDebugPanel.kt new file mode 100644 index 0000000..318ea55 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/debug/ElementDebugPanel.kt @@ -0,0 +1,79 @@ +package com.pixelized.desktop.lwa.ui.composable.scene.debug + +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.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.SceneElement +import com.pixelized.desktop.lwa.ui.theme.lwa +import com.pixelized.desktop.lwa.ui.theme.typography.LwaTypography + +@Composable +fun ElementDebugPanel( + 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.propertyId, + 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.propertyIdSpan) { append("id: ") } + append(element.id) + }, + ) + Text( + modifier = Modifier.padding(start = padding), + style = MaterialTheme.lwa.typography.debug.propertyValue, + text = buildAnnotatedString { + withStyle(style.propertyIdSpan) { append("offset: ") } + append(element.offset.toString()) + }, + ) + Text( + modifier = Modifier.padding(start = padding), + style = MaterialTheme.lwa.typography.debug.propertyValue, + text = buildAnnotatedString { + withStyle(style.propertyIdSpan) { append("size: ") } + append(element.size.toString()) + }, + ) + Text( + modifier = Modifier.padding(start = padding), + style = MaterialTheme.lwa.typography.debug.propertyValue, + text = buildAnnotatedString { + withStyle(style.propertyIdSpan) { append("alpha: ") } + append(element.alpha.toString()) + }, + ) + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/debug/LayerDebugPanel.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/debug/LayerDebugPanel.kt new file mode 100644 index 0000000..c9a5931 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/debug/LayerDebugPanel.kt @@ -0,0 +1,79 @@ +package com.pixelized.desktop.lwa.ui.composable.scene.debug + +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.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.SceneLayer +import com.pixelized.desktop.lwa.ui.theme.lwa +import com.pixelized.desktop.lwa.ui.theme.typography.LwaTypography + +@Composable +fun LayerDebugPanel( + 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.propertyId, + 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.propertyIdSpan) { append("id: ") } + append(layer.id) + }, + ) + Text( + modifier = Modifier.padding(start = padding), + style = MaterialTheme.lwa.typography.debug.propertyValue, + text = buildAnnotatedString { + withStyle(style.propertyIdSpan) { append("offset: ") } + append(layer.offset.toString()) + }, + ) + Text( + modifier = Modifier.padding(start = padding), + style = MaterialTheme.lwa.typography.debug.propertyValue, + text = buildAnnotatedString { + withStyle(style.propertyIdSpan) { append("size: ") } + append(layer.size.toString()) + }, + ) + Text( + modifier = Modifier.padding(start = padding), + style = MaterialTheme.lwa.typography.debug.propertyValue, + text = buildAnnotatedString { + withStyle(style.propertyIdSpan) { append("alpha: ") } + append(layer.alpha.toString()) + }, + ) + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/debug/SceneDebugPanel.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/debug/SceneDebugPanel.kt index d2a0ec0..b488f88 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/debug/SceneDebugPanel.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/debug/SceneDebugPanel.kt @@ -17,8 +17,6 @@ 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.Scene -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 @@ -63,7 +61,7 @@ fun SceneDebugPanel( text = "Layers:(${scene.layers.size})" ) scene.layers.forEach { layer -> - SceneLayerDebug( + LayerDebugPanel( modifier = Modifier.padding(start = padding), isOpen = false, layer = layer, @@ -78,7 +76,7 @@ fun SceneDebugPanel( text = "Elements:(${scene.elements.size})" ) scene.elements.forEach { element -> - SceneElementDebug( + ElementDebugPanel( modifier = Modifier.padding(start = padding), isOpen = false, element = element, diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/drawables/SceneElement.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/drawables/SceneElement.kt index a900c86..ef1e7cd 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/drawables/SceneElement.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/drawables/SceneElement.kt @@ -1,33 +1,21 @@ 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 +import org.jetbrains.compose.resources.DrawableResource +import org.jetbrains.compose.resources.imageResource +import java.util.UUID @Stable class SceneElement( id: String, name: String, texture: ImageBitmap, - offset: IntOffset = IntOffset.Companion.Zero, + offset: IntOffset = IntOffset.Zero, size: IntSize = IntSize(texture.width, texture.height), alpha: Float = 1f, ) : SceneDrawable( @@ -45,60 +33,22 @@ class SceneElement( } @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, +@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, ) - if (isOpen.value) { - Text( - modifier = Modifier.padding(start = padding), - style = MaterialTheme.lwa.typography.debug.propertyValue, - text = buildAnnotatedString { - withStyle(style.propertyIdSpan) { append("id: ") } - append(element.id) - }, - ) - Text( - modifier = Modifier.padding(start = padding), - style = MaterialTheme.lwa.typography.debug.propertyValue, - text = buildAnnotatedString { - withStyle(style.propertyIdSpan) { append("offset: ") } - append(element.offset.toString()) - }, - ) - Text( - modifier = Modifier.padding(start = padding), - style = MaterialTheme.lwa.typography.debug.propertyValue, - text = buildAnnotatedString { - withStyle(style.propertyIdSpan) { append("size: ") } - append(element.size.toString()) - }, - ) - Text( - modifier = Modifier.padding(start = padding), - style = MaterialTheme.lwa.typography.debug.propertyValue, - text = buildAnnotatedString { - withStyle(style.propertyIdSpan) { append("alpha: ") } - append(element.alpha.toString()) - }, - ) - } } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/drawables/SceneLayer.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/drawables/SceneLayer.kt index 4ac34e1..0cccd1f 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/drawables/SceneLayer.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/drawables/SceneLayer.kt @@ -1,33 +1,21 @@ 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 +import org.jetbrains.compose.resources.DrawableResource +import org.jetbrains.compose.resources.imageResource +import java.util.UUID @Stable class SceneLayer( - id : String, + id: String, name: String, texture: ImageBitmap, - offset: IntOffset = IntOffset.Companion.Zero, + offset: IntOffset = IntOffset.Zero, size: IntSize = IntSize(texture.width, texture.height), alpha: Float = 1f, ) : SceneDrawable( @@ -40,60 +28,22 @@ class SceneLayer( ) @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.propertyId, - color = MaterialTheme.lwa.colorScheme.base.primary, - text = layer.name, +@Stable +fun rememberLayoutFromResource( + name: String, + resource: DrawableResource, + offset: IntOffset = IntOffset.Zero, +): SceneLayer { + val texture = imageResource( + resource = resource, + ) + return remember(resource) { + SceneLayer( + id = UUID.randomUUID().toString(), + name = name, + texture = texture, + offset = offset, + alpha = 1f, ) - if (isOpen.value) { - Text( - modifier = Modifier.padding(start = padding), - style = MaterialTheme.lwa.typography.debug.propertyValue, - text = buildAnnotatedString { - withStyle(style.propertyIdSpan) { append("id: ") } - append(layer.id) - }, - ) - Text( - modifier = Modifier.padding(start = padding), - style = MaterialTheme.lwa.typography.debug.propertyValue, - text = buildAnnotatedString { - withStyle(style.propertyIdSpan) { append("offset: ") } - append(layer.offset.toString()) - }, - ) - Text( - modifier = Modifier.padding(start = padding), - style = MaterialTheme.lwa.typography.debug.propertyValue, - text = buildAnnotatedString { - withStyle(style.propertyIdSpan) { append("size: ") } - append(layer.size.toString()) - }, - ) - Text( - modifier = Modifier.padding(start = padding), - style = MaterialTheme.lwa.typography.debug.propertyValue, - text = buildAnnotatedString { - withStyle(style.propertyIdSpan) { append("alpha: ") } - append(layer.alpha.toString()) - }, - ) - } } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/fogOfWar/FogOfWar.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/fogOfWar/FogOfWar.kt index e68ebdb..0e0fb7e 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/fogOfWar/FogOfWar.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/fogOfWar/FogOfWar.kt @@ -1,6 +1,8 @@ package com.pixelized.desktop.lwa.ui.composable.scene.fogOfWar import androidx.compose.runtime.Stable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.neverEqualPolicy import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.draw.drawWithCache @@ -28,14 +30,14 @@ import kotlinx.coroutines.launch class FogOfWar( val color: Color = Color.Black.copy(alpha = 0.5f), ) { - val path = Path() + val path = mutableStateOf(value = Path(), policy = neverEqualPolicy()) fun moveTo(position: Offset) { - path.moveTo(x = position.x, y = position.y) + path.value = path.value.also { it.moveTo(x = position.x, y = position.y) } } fun lineTo(position: Offset) { - path.lineTo(x = position.x, y = position.y) + path.value = path.value.also { it.lineTo(x = position.x, y = position.y) } } companion object { diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/fogOfWar/Modifier+drawFogOfWar.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/fogOfWar/Modifier+drawFogOfWar.kt index 0e8e7ec..01109c8 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/fogOfWar/Modifier+drawFogOfWar.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/fogOfWar/Modifier+drawFogOfWar.kt @@ -42,7 +42,7 @@ fun Modifier.drawFogOfWar( top = -camera.offset.y.toFloat(), ) { drawPath( - path = fogOfWar.path, + path = fogOfWar.path.value, style = stroke, color = color, blendMode = BlendMode.Clear, diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/utils/IntOffset+Coordinate.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/utils/IntOffset+Coordinate.kt new file mode 100644 index 0000000..c01cc1a --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/utils/IntOffset+Coordinate.kt @@ -0,0 +1,104 @@ +package com.pixelized.desktop.lwa.ui.composable.scene.utils + +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.util.fastRoundToInt +import com.pixelized.desktop.lwa.ui.composable.scene.camera.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 IntOffset.global( + camera: Camera, +): Offset = global( + zoom = camera.zoom, + offset = camera.offset, +) + +/** + * 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 IntOffset.global( + zoom: Float, + offset: IntOffset, +): Offset = Offset( + x = this.x * zoom + offset.x, + y = this.y * zoom + 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 IntOffset.local( + camera: Camera, +): Offset = local( + zoom = camera.zoom, + offset = camera.offset, +) + +/** + * 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( + zoom: Float, + offset: IntOffset, +): Offset = Offset( + x = (this.x - offset.x) / zoom, + y = (this.y - offset.y) / zoom, +) + + +/** + * 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 IntOffset.globalInt( + camera: Camera, +): IntOffset = globalInt( + zoom = camera.zoom, + offset = camera.offset, +) + + +/** + * 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 IntOffset.globalInt( + zoom: Float, + offset: IntOffset, +): IntOffset = IntOffset( + x = (this.x * zoom + offset.x).fastRoundToInt(), + y = (this.y * zoom + offset.y).fastRoundToInt(), +) + +/** + * 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.localInt( + camera: Camera, +): IntOffset = localInt( + zoom = camera.zoom, + offset = camera.offset, +) + +/** + * 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.localInt( + zoom: Float, + offset: IntOffset, +): IntOffset = IntOffset( + x = ((this.x - offset.x) / zoom).fastRoundToInt(), + y = ((this.y - offset.y) / zoom).fastRoundToInt(), +) \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/utils/Offset+Coordinate.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/utils/Offset+Coordinate.kt index 603a99f..7ca9b33 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/utils/Offset+Coordinate.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/utils/Offset+Coordinate.kt @@ -2,6 +2,7 @@ package com.pixelized.desktop.lwa.ui.composable.scene.utils import androidx.compose.ui.geometry.Offset import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.util.fastRoundToInt import com.pixelized.desktop.lwa.ui.composable.scene.camera.Camera /** @@ -11,9 +12,22 @@ import com.pixelized.desktop.lwa.ui.composable.scene.camera.Camera */ fun Offset.global( camera: Camera, +): Offset = global( + zoom = camera.zoom, + offset = camera.offset, +) + +/** + * 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( + zoom: Float, + offset: IntOffset, ): Offset = Offset( - x = this.x * camera.zoom + camera.offset.x, - y = this.y * camera.zoom + camera.offset.y, + x = this.x * zoom + offset.x, + y = this.y * zoom + offset.y, ) /** @@ -22,18 +36,67 @@ fun Offset.global( */ fun Offset.local( camera: Camera, -): Offset = Offset( - x = (this.x - camera.offset.x) / camera.zoom, - y = (this.y - camera.offset.y) / camera.zoom, +): Offset = local( + zoom = camera.zoom, + offset = camera.offset, ) /** * 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, +fun Offset.local( + zoom: Float, + offset: IntOffset, ): Offset = Offset( - x = (this.x.toFloat() - camera.offset.x) / camera.zoom, - y = (this.y.toFloat() - camera.offset.y) / camera.zoom, + x = (this.x - offset.x) / zoom, + y = (this.y - offset.y) / zoom, +) + +/** + * 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.globalInt( + camera: Camera, +): IntOffset = globalInt( + zoom = camera.zoom, + offset = camera.offset, +) + +/** + * 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.globalInt( + zoom: Float, + offset: IntOffset, +): IntOffset = IntOffset( + x = (this.x * zoom + offset.x).fastRoundToInt(), + y = (this.y * zoom + offset.y).fastRoundToInt(), +) + +/** + * 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.localInt( + camera: Camera, +): IntOffset = localInt( + zoom = camera.zoom, + offset = camera.offset, +) + +/** + * 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.localInt( + zoom: Float, + offset: IntOffset, +): IntOffset = IntOffset( + x = ((this.x - offset.x) / zoom).fastRoundToInt(), + y = ((this.y - offset.y) / zoom).fastRoundToInt(), ) \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/map/DahomeMap.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/map/DahomeMap.kt index 205cb2b..6deb722 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/map/DahomeMap.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/map/DahomeMap.kt @@ -19,7 +19,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel import com.pixelized.desktop.lwa.ui.composable.scene.Scene import com.pixelized.desktop.lwa.ui.composable.scene.camera.Camera import com.pixelized.desktop.lwa.ui.composable.scene.cursor.Cursor @@ -27,7 +26,7 @@ import com.pixelized.desktop.lwa.ui.composable.scene.cursor.onCursorControl import com.pixelized.desktop.lwa.ui.composable.scene.debug.CameraDebugPanel import com.pixelized.desktop.lwa.ui.composable.scene.debug.CursorDebugPanel import com.pixelized.desktop.lwa.ui.composable.scene.debug.SceneDebugPanel -import com.pixelized.desktop.lwa.ui.composable.scene.rememberLayoutFromResource +import com.pixelized.desktop.lwa.ui.composable.scene.drawables.rememberLayoutFromResource import com.pixelized.desktop.lwa.ui.theme.lwa import kotlinx.coroutines.launch import lwacharactersheet.composeapp.generated.resources.Res @@ -58,7 +57,7 @@ fun MapScene( val camera = remember { Camera( initialZoom = 1f, - initialOffset = IntOffset(x = -150, y = -120), + initialOffset = IntOffset(x = 1407, y = 1520), ) } val cursor = remember { @@ -86,22 +85,14 @@ fun MapScene( .padding(paddingValues = MaterialTheme.lwa.dimen.paddingValues), enableDebug = enableDebug, onZoomIn = { - scope.launch { - camera.handleZoom(power = 0.3f) - } + camera.handleZoom(power = 0.15f) }, onZoomOut = { - scope.launch { - camera.handleZoom(power = -0.3f) - } + camera.handleZoom(power = -0.15f) }, onResetCamera = { - scope.launch { - camera.resetPosition() - } - scope.launch { - camera.resetZoom() - } + camera.resetPosition() + camera.resetZoom() }, onToggleLayer = { scope.launch { @@ -136,6 +127,7 @@ fun MapScene( camera = camera, ) CursorDebugPanel( + camera = camera, cursors = remember { listOf(cursor) }, ) }