Refactor the camera scene (camera) system.
This commit is contained in:
parent
ce05e6a4c4
commit
3485b8a9fd
14 changed files with 453 additions and 298 deletions
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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,38 +29,34 @@ 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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.onPointerEvent(PointerEventType.Scroll) { event: PointerEvent ->
|
||||
scope.launch {
|
||||
val change = event.changes.first()
|
||||
camera.handleZoom(
|
||||
power = -event.changes.first().scrollDelta.y.sign * 0.15f,
|
||||
snap = false,
|
||||
power = -change.scrollDelta.y.sign * 0.15f,
|
||||
target = change.position.round(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private data class CursorDelta(
|
||||
var lastDeltaTimestamp: Long = System.currentTimeMillis(),
|
||||
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) {
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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())
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
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())
|
||||
},
|
||||
return remember(resource) {
|
||||
SceneElement(
|
||||
id = UUID.randomUUID().toString(),
|
||||
name = name,
|
||||
texture = texture,
|
||||
offset = offset,
|
||||
alpha = 1f,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
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,
|
||||
)
|
||||
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())
|
||||
},
|
||||
return remember(resource) {
|
||||
SceneLayer(
|
||||
id = UUID.randomUUID().toString(),
|
||||
name = name,
|
||||
texture = texture,
|
||||
offset = offset,
|
||||
alpha = 1f,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
)
|
||||
|
|
@ -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(),
|
||||
)
|
||||
|
|
@ -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()
|
||||
}
|
||||
},
|
||||
onToggleLayer = {
|
||||
scope.launch {
|
||||
|
|
@ -136,6 +127,7 @@ fun MapScene(
|
|||
camera = camera,
|
||||
)
|
||||
CursorDebugPanel(
|
||||
camera = camera,
|
||||
cursors = remember { listOf(cursor) },
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue