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.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),
)
}
}
}

View file

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

View file

@ -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) {

View file

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

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 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,

View file

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

View file

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

View file

@ -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 {

View file

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

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

View file

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