Refactor the camera scene (camera) system.

This commit is contained in:
Thomas Andres Gomez 2025-12-06 14:24:37 +01:00
parent ce05e6a4c4
commit 3485b8a9fd
14 changed files with 453 additions and 298 deletions

View file

@ -4,25 +4,16 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.BoxScope
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.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.ExperimentalComposeUiApi 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.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.unit.IntOffset
import androidx.compose.ui.unit.IntSize 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.Camera
import com.pixelized.desktop.lwa.ui.composable.scene.camera.onCameraControl 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.SceneElement
import com.pixelized.desktop.lwa.ui.composable.scene.drawables.SceneLayer 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 @Stable
data class Scene( data class Scene(
@ -43,12 +34,12 @@ fun Scene(
scene: Scene, scene: Scene,
content: @Composable BoxScope.() -> Unit, content: @Composable BoxScope.() -> Unit,
) { ) {
val scope = rememberCoroutineScope() rememberCoroutineScope()
Box( Box(
modifier = Modifier modifier = Modifier
.graphicsLayer { clip = true } .graphicsLayer { clip = true }
.onCameraControl(scope = scope, sceneSize = scene.size, camera = camera) .onCameraControl(sceneSize = scene.size, camera = camera)
.drawLayers(camera = camera, layers = scene.layers) .drawLayers(camera = camera, layers = scene.layers)
.drawElements(camera = camera, elements = scene.elements) .drawElements(camera = camera, elements = scene.elements)
.then(other = modifier), .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) @OptIn(ExperimentalComposeUiApi::class)
fun Modifier.drawLayers( fun Modifier.drawLayers(
camera: Camera, camera: Camera,
@ -130,12 +79,6 @@ fun Modifier.drawElements(
dstSize = camera.cameraSize, dstSize = camera.cameraSize,
alpha = element.alpha, alpha = element.alpha,
) )
drawRect(
color = Color.Red,
topLeft = element.position.local(camera = camera),
size = (element.size).toSize() / camera.zoom,
style = Stroke(width = 2f),
)
} }
} }
} }

View file

@ -1,13 +1,12 @@
package com.pixelized.desktop.lwa.ui.composable.scene.camera 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.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.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
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.util.fastRoundToInt import androidx.compose.ui.util.fastRoundToInt
@ -17,22 +16,11 @@ class Camera(
private val initialZoom: Float = 2f, private val initialZoom: Float = 2f,
private val initialOffset: IntOffset = IntOffset.Zero, private val initialOffset: IntOffset = IntOffset.Zero,
) { ) {
private var _zoom = Animatable( private var _zoom = mutableStateOf(initialZoom)
initialValue = initialZoom, val zoom: Float by _zoom
typeConverter = Float.VectorConverter,
)
val zoom: Float get() = _zoom.value
private var _offset = Animatable( private var _offset = mutableStateOf(initialOffset)
initialValue = initialOffset, val offset: IntOffset by _offset
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 _sceneSize: IntSize by mutableStateOf(IntSize.Zero) private var _sceneSize: IntSize by mutableStateOf(IntSize.Zero)
private var _cameraSize: IntSize by mutableStateOf(IntSize.Zero) private var _cameraSize: IntSize by mutableStateOf(IntSize.Zero)
@ -52,46 +40,48 @@ class Camera(
_sceneSize = sceneSize _sceneSize = sceneSize
} }
suspend fun handlePanning( fun handlePanning(
delta: Offset, delta: Offset,
snap: Boolean,
) { ) {
val value = _offset.value - IntOffset( val value = _offset.value - IntOffset(
x = (delta.x * zoom).fastRoundToInt(), x = (delta.x * zoom).fastRoundToInt(),
y = (delta.y * zoom).fastRoundToInt(), y = (delta.y * zoom).fastRoundToInt(),
) )
when { _offset.value = value
snap -> _offset.snapTo(targetValue = value)
else -> _offset.animateTo(targetValue = value)
}
} }
suspend fun handleZoom( fun handleZoom(
power: Float, power: Float,
snap: Boolean = false, target: IntOffset? = null,
) { ) {
val value = _zoom.value * (1f - power) val zoomTarget = _zoom.value * (1f - power)
when {
snap -> _zoom.snapTo(targetValue = value) val projection = cameraSizeProjection(
else -> _zoom.animateTo(targetValue = value) 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( fun resetPosition() {
snap: Boolean = false, _offset.value = initialOffset
) {
when (snap) {
true -> _offset.snapTo(targetValue = initialOffset)
else -> _offset.animateTo(targetValue = initialOffset)
}
} }
suspend fun resetZoom( fun resetZoom() {
snap: Boolean = false, _zoom.value = initialZoom
) {
when (snap) {
true -> _zoom.snapTo(targetValue = initialZoom)
else -> _zoom.animateTo(targetValue = initialZoom)
}
} }
private fun cameraSizeProjection(zoomTarget: Float): Size = Size(
width = cameraSize.width * _zoom.value - cameraSize.width * zoomTarget,
height = cameraSize.height * _zoom.value - cameraSize.height * zoomTarget,
)
} }

View file

@ -12,13 +12,11 @@ import androidx.compose.ui.input.pointer.isTertiaryPressed
import androidx.compose.ui.input.pointer.onPointerEvent import androidx.compose.ui.input.pointer.onPointerEvent
import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.IntSize
import kotlinx.coroutines.CoroutineScope import androidx.compose.ui.unit.round
import kotlinx.coroutines.launch
import kotlin.math.sign import kotlin.math.sign
@OptIn(ExperimentalFoundationApi::class, ExperimentalComposeUiApi::class) @OptIn(ExperimentalFoundationApi::class, ExperimentalComposeUiApi::class)
fun Modifier.onCameraControl( fun Modifier.onCameraControl(
scope: CoroutineScope,
sceneSize: IntSize, sceneSize: IntSize,
camera: Camera, camera: Camera,
): Modifier { ): Modifier {
@ -31,26 +29,22 @@ fun Modifier.onCameraControl(
) )
} }
.onPointerEvent(PointerEventType.Move) { event: PointerEvent -> .onPointerEvent(PointerEventType.Move) { event: PointerEvent ->
scope.launch { offsetDelta.handlePositionChange(
offsetDelta.handlePositionChange( event = event,
event = event, ) { delta ->
) { delta -> when {
when { event.buttons.isTertiaryPressed || (event.keyboardModifiers.isCtrlPressed && event.buttons.isPrimaryPressed) -> camera.handlePanning(
event.buttons.isTertiaryPressed || (event.keyboardModifiers.isCtrlPressed && event.buttons.isPrimaryPressed) -> camera.handlePanning( delta = delta,
delta = delta, )
snap = true,
)
}
} }
} }
} }
.onPointerEvent(PointerEventType.Scroll) { event: PointerEvent -> .onPointerEvent(PointerEventType.Scroll) { event: PointerEvent ->
scope.launch { val change = event.changes.first()
camera.handleZoom( camera.handleZoom(
power = -event.changes.first().scrollDelta.y.sign * 0.15f, power = -change.scrollDelta.y.sign * 0.15f,
snap = false, target = change.position.round(),
) )
}
} }
} }
@ -59,10 +53,10 @@ private data class CursorDelta(
var previousPosition: Offset = Offset.Zero, var previousPosition: Offset = Offset.Zero,
var currentPosition: Offset = Offset.Zero, var currentPosition: Offset = Offset.Zero,
) { ) {
suspend inline fun handlePositionChange( inline fun handlePositionChange(
event: PointerEvent, event: PointerEvent,
delay: Float = 10f, delay: Float = 10f,
crossinline block: suspend (delta: Offset) -> Unit, crossinline block: (delta: Offset) -> Unit,
) { ) {
val currentTimestamp = System.currentTimeMillis() val currentTimestamp = System.currentTimeMillis()
if (currentTimestamp - lastDeltaTimestamp > delay) { if (currentTimestamp - lastDeltaTimestamp > delay) {

View file

@ -16,7 +16,9 @@ import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.withStyle import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
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.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.lwa
import com.pixelized.desktop.lwa.ui.theme.typography.LwaTypography import com.pixelized.desktop.lwa.ui.theme.typography.LwaTypography
@ -26,6 +28,7 @@ fun CursorDebugPanel(
style: LwaTypography.Debug = MaterialTheme.lwa.typography.debug, style: LwaTypography.Debug = MaterialTheme.lwa.typography.debug,
padding: Dp = MaterialTheme.lwa.dimen.debug.offset, padding: Dp = MaterialTheme.lwa.dimen.debug.offset,
spacing: Dp = 2.dp, spacing: Dp = 2.dp,
camera: Camera,
cursors: List<Cursor>, cursors: List<Cursor>,
isOpen: Boolean = true, isOpen: Boolean = true,
) { ) {
@ -50,10 +53,18 @@ fun CursorDebugPanel(
modifier = Modifier.padding(start = padding), modifier = Modifier.padding(start = padding),
style = MaterialTheme.lwa.typography.debug.propertyValue, style = MaterialTheme.lwa.typography.debug.propertyValue,
text = buildAnnotatedString { text = buildAnnotatedString {
withStyle(style.propertyIdSpan) { append("coordinate: ") } withStyle(style.propertyIdSpan) { append("global: ") }
append(cursor.offset.toString()) 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())
},
)
} }
} }
} }

View file

@ -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())
},
)
}
}
}

View file

@ -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())
},
)
}
}
}

View file

@ -17,8 +17,6 @@ import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
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.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.lwa
import com.pixelized.desktop.lwa.ui.theme.typography.LwaTypography import com.pixelized.desktop.lwa.ui.theme.typography.LwaTypography
@ -63,7 +61,7 @@ fun SceneDebugPanel(
text = "Layers:(${scene.layers.size})" text = "Layers:(${scene.layers.size})"
) )
scene.layers.forEach { layer -> scene.layers.forEach { layer ->
SceneLayerDebug( LayerDebugPanel(
modifier = Modifier.padding(start = padding), modifier = Modifier.padding(start = padding),
isOpen = false, isOpen = false,
layer = layer, layer = layer,
@ -78,7 +76,7 @@ fun SceneDebugPanel(
text = "Elements:(${scene.elements.size})" text = "Elements:(${scene.elements.size})"
) )
scene.elements.forEach { element -> scene.elements.forEach { element ->
SceneElementDebug( ElementDebugPanel(
modifier = Modifier.padding(start = padding), modifier = Modifier.padding(start = padding),
isOpen = false, isOpen = false,
element = element, element = element,

View file

@ -1,33 +1,21 @@
package com.pixelized.desktop.lwa.ui.composable.scene.drawables 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.Composable
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ImageBitmap 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.IntOffset
import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp import org.jetbrains.compose.resources.DrawableResource
import com.pixelized.desktop.lwa.ui.theme.lwa import org.jetbrains.compose.resources.imageResource
import com.pixelized.desktop.lwa.ui.theme.typography.LwaTypography import java.util.UUID
@Stable @Stable
class SceneElement( class SceneElement(
id: String, id: String,
name: String, name: String,
texture: ImageBitmap, texture: ImageBitmap,
offset: IntOffset = IntOffset.Companion.Zero, offset: IntOffset = IntOffset.Zero,
size: IntSize = IntSize(texture.width, texture.height), size: IntSize = IntSize(texture.width, texture.height),
alpha: Float = 1f, alpha: Float = 1f,
) : SceneDrawable( ) : SceneDrawable(
@ -45,60 +33,22 @@ class SceneElement(
} }
@Composable @Composable
fun SceneElementDebug( @Stable
modifier: Modifier = Modifier, fun rememberElementFromResource(
element: SceneElement, name: String,
isOpen: Boolean = true, resource: DrawableResource,
style: LwaTypography.Debug = MaterialTheme.lwa.typography.debug, offset: IntOffset = IntOffset.Zero,
padding: Dp = MaterialTheme.lwa.dimen.debug.offset, ): SceneElement {
) { val texture = imageResource(
val isOpen = remember { mutableStateOf(isOpen) } resource = resource,
)
Column( return remember(resource) {
modifier = Modifier SceneElement(
.clickable { isOpen.value = isOpen.value.not() } id = UUID.randomUUID().toString(),
.widthIn(min = MaterialTheme.lwa.dimen.debug.panelWidth) name = name,
.then(other = modifier), texture = texture,
verticalArrangement = Arrangement.spacedBy(space = 2.dp), offset = offset,
) { alpha = 1f,
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.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())
},
)
}
} }
} }

View file

@ -1,33 +1,21 @@
package com.pixelized.desktop.lwa.ui.composable.scene.drawables 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.Composable
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ImageBitmap 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.IntOffset
import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp import org.jetbrains.compose.resources.DrawableResource
import com.pixelized.desktop.lwa.ui.theme.lwa import org.jetbrains.compose.resources.imageResource
import com.pixelized.desktop.lwa.ui.theme.typography.LwaTypography import java.util.UUID
@Stable @Stable
class SceneLayer( class SceneLayer(
id : String, id: String,
name: String, name: String,
texture: ImageBitmap, texture: ImageBitmap,
offset: IntOffset = IntOffset.Companion.Zero, offset: IntOffset = IntOffset.Zero,
size: IntSize = IntSize(texture.width, texture.height), size: IntSize = IntSize(texture.width, texture.height),
alpha: Float = 1f, alpha: Float = 1f,
) : SceneDrawable( ) : SceneDrawable(
@ -40,60 +28,22 @@ class SceneLayer(
) )
@Composable @Composable
fun SceneLayerDebug( @Stable
modifier: Modifier = Modifier, fun rememberLayoutFromResource(
layer: SceneLayer, name: String,
isOpen: Boolean = true, resource: DrawableResource,
style: LwaTypography.Debug = MaterialTheme.lwa.typography.debug, offset: IntOffset = IntOffset.Zero,
padding: Dp = MaterialTheme.lwa.dimen.debug.offset, ): SceneLayer {
) { val texture = imageResource(
val isOpen = remember { mutableStateOf(isOpen) } resource = resource,
)
Column( return remember(resource) {
modifier = Modifier SceneLayer(
.clickable { isOpen.value = isOpen.value.not() } id = UUID.randomUUID().toString(),
.widthIn(min = MaterialTheme.lwa.dimen.debug.panelWidth) name = name,
.then(other = modifier), texture = texture,
verticalArrangement = Arrangement.spacedBy(space = 2.dp), offset = offset,
) { alpha = 1f,
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())
},
)
}
} }
} }

View file

@ -1,6 +1,8 @@
package com.pixelized.desktop.lwa.ui.composable.scene.fogOfWar package com.pixelized.desktop.lwa.ui.composable.scene.fogOfWar
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.neverEqualPolicy
import androidx.compose.ui.ExperimentalComposeUiApi 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
@ -28,14 +30,14 @@ import kotlinx.coroutines.launch
class FogOfWar( class FogOfWar(
val color: Color = Color.Black.copy(alpha = 0.5f), val color: Color = Color.Black.copy(alpha = 0.5f),
) { ) {
val path = Path() val path = mutableStateOf(value = Path(), policy = neverEqualPolicy())
fun moveTo(position: Offset) { 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) { 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 { companion object {

View file

@ -42,7 +42,7 @@ fun Modifier.drawFogOfWar(
top = -camera.offset.y.toFloat(), top = -camera.offset.y.toFloat(),
) { ) {
drawPath( drawPath(
path = fogOfWar.path, path = fogOfWar.path.value,
style = stroke, style = stroke,
color = color, color = color,
blendMode = BlendMode.Clear, blendMode = BlendMode.Clear,

View file

@ -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(),
)

View file

@ -2,6 +2,7 @@ package com.pixelized.desktop.lwa.ui.composable.scene.utils
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.util.fastRoundToInt
import com.pixelized.desktop.lwa.ui.composable.scene.camera.Camera 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( fun Offset.global(
camera: Camera, 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( ): Offset = Offset(
x = this.x * camera.zoom + camera.offset.x, x = this.x * zoom + offset.x,
y = this.y * camera.zoom + camera.offset.y, y = this.y * zoom + offset.y,
) )
/** /**
@ -22,18 +36,67 @@ fun Offset.global(
*/ */
fun Offset.local( fun Offset.local(
camera: Camera, camera: Camera,
): Offset = Offset( ): Offset = local(
x = (this.x - camera.offset.x) / camera.zoom, zoom = camera.zoom,
y = (this.y - camera.offset.y) / camera.zoom, offset = camera.offset,
) )
/** /**
* Convert global positon to local one. * Convert global positon to local one.
* Local position take into account the camera and are use to display stuff on the Scene composable. * Local position take into account the camera and are use to display stuff on the Scene composable.
*/ */
fun IntOffset.local( fun Offset.local(
camera: Camera, zoom: Float,
offset: IntOffset,
): Offset = Offset( ): Offset = Offset(
x = (this.x.toFloat() - camera.offset.x) / camera.zoom, x = (this.x - offset.x) / zoom,
y = (this.y.toFloat() - camera.offset.y) / camera.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(),
) )

View file

@ -19,7 +19,6 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp 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.Scene
import com.pixelized.desktop.lwa.ui.composable.scene.camera.Camera 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.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.CameraDebugPanel
import com.pixelized.desktop.lwa.ui.composable.scene.debug.CursorDebugPanel 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.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 com.pixelized.desktop.lwa.ui.theme.lwa
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import lwacharactersheet.composeapp.generated.resources.Res import lwacharactersheet.composeapp.generated.resources.Res
@ -58,7 +57,7 @@ fun MapScene(
val camera = remember { val camera = remember {
Camera( Camera(
initialZoom = 1f, initialZoom = 1f,
initialOffset = IntOffset(x = -150, y = -120), initialOffset = IntOffset(x = 1407, y = 1520),
) )
} }
val cursor = remember { val cursor = remember {
@ -86,22 +85,14 @@ fun MapScene(
.padding(paddingValues = MaterialTheme.lwa.dimen.paddingValues), .padding(paddingValues = MaterialTheme.lwa.dimen.paddingValues),
enableDebug = enableDebug, enableDebug = enableDebug,
onZoomIn = { onZoomIn = {
scope.launch { camera.handleZoom(power = 0.15f)
camera.handleZoom(power = 0.3f)
}
}, },
onZoomOut = { onZoomOut = {
scope.launch { camera.handleZoom(power = -0.15f)
camera.handleZoom(power = -0.3f)
}
}, },
onResetCamera = { onResetCamera = {
scope.launch { camera.resetPosition()
camera.resetPosition() camera.resetZoom()
}
scope.launch {
camera.resetZoom()
}
}, },
onToggleLayer = { onToggleLayer = {
scope.launch { scope.launch {
@ -136,6 +127,7 @@ fun MapScene(
camera = camera, camera = camera,
) )
CursorDebugPanel( CursorDebugPanel(
camera = camera,
cursors = remember { listOf(cursor) }, cursors = remember { listOf(cursor) },
) )
} }