Add basic fog of war feature.

This commit is contained in:
Thomas Andres Gomez 2025-10-29 20:52:46 +01:00
parent ae2c3da582
commit 3d5f29c18c
6 changed files with 171 additions and 20 deletions

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="#000000"
android:pathData="M720,760Q703,760 691.5,748.5Q680,737 680,720Q680,703 691.5,691.5Q703,680 720,680Q737,680 748.5,691.5Q760,703 760,720Q760,737 748.5,748.5Q737,760 720,760ZM280,880Q263,880 251.5,868.5Q240,857 240,840Q240,823 251.5,811.5Q263,800 280,800Q297,800 308.5,811.5Q320,823 320,840Q320,857 308.5,868.5Q297,880 280,880ZM240,760Q223,760 211.5,748.5Q200,737 200,720Q200,703 211.5,691.5Q223,680 240,680L600,680Q617,680 628.5,691.5Q640,703 640,720Q640,737 628.5,748.5Q617,760 600,760L240,760ZM400,880Q383,880 371.5,868.5Q360,857 360,840Q360,823 371.5,811.5Q383,800 400,800L680,800Q697,800 708.5,811.5Q720,823 720,840Q720,857 708.5,868.5Q697,880 680,880L400,880ZM300,640Q209,640 144.5,575.5Q80,511 80,420Q80,337 135,275Q190,213 271,202Q303,145 358.5,112.5Q414,80 480,80Q570,80 636.5,137.5Q703,195 717,281Q786,287 833,338Q880,389 880,460Q880,535 827.5,587.5Q775,640 700,640L300,640ZM300,560L700,560Q742,560 771,531Q800,502 800,460Q800,418 771,389Q742,360 700,360L640,360L640,320Q640,254 593,207Q546,160 480,160Q432,160 392.5,186Q353,212 333,256L323,280L298,280Q241,282 200.5,322.5Q160,363 160,420Q160,478 201,519Q242,560 300,560ZM480,360L480,360Q480,360 480,360Q480,360 480,360Q480,360 480,360Q480,360 480,360L480,360L480,360Q480,360 480,360Q480,360 480,360Q480,360 480,360Q480,360 480,360L480,360L480,360Q480,360 480,360Q480,360 480,360Q480,360 480,360Q480,360 480,360Z" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="#000000"
android:pathData="M720,760Q703,760 691.5,748.5Q680,737 680,720Q680,703 691.5,691.5Q703,680 720,680Q737,680 748.5,691.5Q760,703 760,720Q760,737 748.5,748.5Q737,760 720,760ZM280,880Q263,880 251.5,868.5Q240,857 240,840Q240,823 251.5,811.5Q263,800 280,800Q297,800 308.5,811.5Q320,823 320,840Q320,857 308.5,868.5Q297,880 280,880ZM240,760Q223,760 211.5,748.5Q200,737 200,720Q200,703 211.5,691.5Q223,680 240,680L600,680Q617,680 628.5,691.5Q640,703 640,720Q640,737 628.5,748.5Q617,760 600,760L240,760ZM400,880Q383,880 371.5,868.5Q360,857 360,840Q360,823 371.5,811.5Q383,800 400,800L680,800Q697,800 708.5,811.5Q720,823 720,840Q720,857 708.5,868.5Q697,880 680,880L400,880ZM300,640Q209,640 144.5,575.5Q80,511 80,420Q80,337 135,275Q190,213 271,202Q303,145 358.5,112.5Q414,80 480,80Q570,80 636.5,137.5Q703,195 717,281Q786,287 833,338Q880,389 880,460Q880,535 827.5,587.5Q775,640 700,640L300,640Z" />
</vector>

View file

@ -1,20 +1,22 @@
package com.pixelized.desktop.lwa.ui.composable.scene
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import com.pixelized.desktop.lwa.ui.screen.campaign.LocalCampaignLayoutScope
import kotlinx.coroutines.launch
import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.ic_center_focus_weak_24dp
import lwacharactersheet.composeapp.generated.resources.ic_foggy_24dp
import lwacharactersheet.composeapp.generated.resources.ic_foggy_filled_24dp
import lwacharactersheet.composeapp.generated.resources.ic_visibility_24dp
import lwacharactersheet.composeapp.generated.resources.ic_zoom_in_map_24dp
import lwacharactersheet.composeapp.generated.resources.ic_zoom_out_map_24dp
@ -29,6 +31,7 @@ fun MapScene(
) {
val campaign = LocalCampaignLayoutScope.current
val scope = rememberCoroutineScope()
val fogOfWarEdit = remember { mutableStateOf(true) }
val map = rememberLayoutFromResource(
name = "Dahomé",
@ -68,7 +71,7 @@ fun MapScene(
initialZoom = 1f,
initialOffset = IntOffset(x = -150, y = -120),
),
fogOfWar = FogOfWar.NONE,
fogOfWar = FogOfWar(),
layers = listOf(
map,
mapRegionOverlay,
@ -81,14 +84,21 @@ fun MapScene(
)
}
Scene(
modifier = modifier,
modifier = modifier.onFogOfWarControl(
scope = scope,
enable = fogOfWarEdit.value,
fogOfWar = scene.fogOfWar,
camera = scene.camera,
),
scene = scene,
) {
Column(
Row(
modifier = Modifier
.align(alignment = Alignment.BottomEnd)
.padding(end = campaign.rightPanel.value.width)
.padding(all = 8.dp)
.align(alignment = Alignment.BottomStart)
.padding(
start = campaign.leftPanel.value.width,
bottom = campaign.chatOverlay.value.height,
)
) {
IconButton(
onClick = {
@ -149,6 +159,19 @@ fun MapScene(
contentDescription = null
)
}
IconButton(
onClick = {
fogOfWarEdit.value = fogOfWarEdit.value.not()
}
) {
Icon(
painter = when (fogOfWarEdit.value) {
true -> painterResource(Res.drawable.ic_foggy_filled_24dp)
else -> painterResource(Res.drawable.ic_foggy_24dp)
},
contentDescription = null
)
}
}
}
}

View file

@ -1,13 +1,78 @@
package com.pixelized.desktop.lwa.ui.composable.scene
import androidx.compose.runtime.Stable
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.input.pointer.PointerEvent
import androidx.compose.ui.input.pointer.PointerEventType
import androidx.compose.ui.input.pointer.isPrimaryPressed
import androidx.compose.ui.input.pointer.onPointerEvent
import com.pixelized.desktop.lwa.ui.composable.scene.utils.global
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@Stable
data class FogOfWar(
val color: Color = Color.Black.copy(alpha = 0.0f),
class FogOfWar(
val color: Color = Color.Black.copy(alpha = 0.5f),
) {
val path = Path()
fun moveTo(position: Offset) {
path.moveTo(x = position.x, y = position.y)
}
fun lineTo(position: Offset) {
path.lineTo(x = position.x, y = position.y)
}
companion object {
val NONE = FogOfWar(color = Color.Transparent)
}
}
@OptIn(ExperimentalComposeUiApi::class)
fun Modifier.onFogOfWarControl(
scope: CoroutineScope,
enable: Boolean,
camera: Camera,
fogOfWar: FogOfWar,
) = if (!enable) {
this
} else {
var lastEventTime = System.currentTimeMillis()
var previousEvent: PointerEvent? = null
this
.onPointerEvent(PointerEventType.Release) { event: PointerEvent ->
scope.launch {
println("PointerEventType.Release")
lastEventTime = System.currentTimeMillis()
previousEvent = null
}
}
.onPointerEvent(PointerEventType.Move) { event: PointerEvent ->
scope.launch {
val pointer = event.changes.firstOrNull()
val time = pointer?.uptimeMillis ?: 0L
if (time - lastEventTime > 10L && event.buttons.isPrimaryPressed) {
if (previousEvent?.buttons?.isPrimaryPressed == true) {
println("PointerEventType.LineTo")
pointer?.position
?.global(camera = camera)
?.let(fogOfWar::lineTo)
} else {
println("PointerEventType.MoveTo")
pointer?.position
?.global(camera = camera)
?.let(fogOfWar::moveTo)
}
lastEventTime = System.currentTimeMillis()
previousEvent = event
}
}
}
}

View file

@ -15,8 +15,14 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.CompositingStrategy
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.StrokeJoin
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.drawscope.scale
import androidx.compose.ui.graphics.drawscope.translate
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.PointerEvent
import androidx.compose.ui.input.pointer.PointerEventType
@ -55,7 +61,6 @@ data class Scene(
}
@Composable
fun Scene(
modifier: Modifier = Modifier,
@ -253,9 +258,38 @@ fun Modifier.drawElements(
fun Modifier.drawFogOfWar(
scene: Scene,
): Modifier = this.drawWithCache {
): Modifier = this
.graphicsLayer(
compositingStrategy = CompositingStrategy.Offscreen
)
.drawWithCache {
val stroke = Stroke(
width = 32f,
cap = StrokeCap.Round,
join = StrokeJoin.Round
)
val fog = Color.Black.copy(alpha = 0.5f)
val color = Color.Transparent
onDrawBehind {
drawRect(color = scene.fogOfWar.color)
drawRect(
color = fog,
)
scale(
scale = 1 / scene.camera.zoom,
pivot = Offset.Zero,
) {
translate(
left = -scene.camera.offset.x.toFloat(),
top = -scene.camera.offset.y.toFloat(),
) {
drawPath(
path = scene.fogOfWar.path,
style = stroke,
color = color,
blendMode = BlendMode.Clear,
)
}
}
}
}

View file

@ -1,14 +1,16 @@
package com.pixelized.desktop.lwa.ui.composable.scene
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Card
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
@ -31,7 +33,9 @@ fun SceneDebugPanel(
scene: Scene,
) {
Card(
modifier = modifier,
modifier = Modifier
.verticalScroll(state = rememberScrollState())
.then(other = modifier),
backgroundColor = MaterialTheme.lwa.colorScheme.elevated.base4dp,
) {
Column(
@ -39,12 +43,15 @@ fun SceneDebugPanel(
verticalArrangement = Arrangement.spacedBy(space = 8.dp),
) {
SceneDebug(
modifier = Modifier.animateContentSize(),
scene = scene,
)
SceneCursorDebug(
modifier = Modifier.animateContentSize(),
cursor = cursor,
)
SceneCameraDebug(
modifier = Modifier.animateContentSize(),
camera = scene.camera,
)
Column {
@ -54,7 +61,9 @@ fun SceneDebugPanel(
)
scene.layers.forEach { layer ->
SceneLayerDebug(
modifier = Modifier.padding(start = MaterialTheme.lwa.dimen.debug.offset),
modifier = Modifier
.animateContentSize()
.padding(start = MaterialTheme.lwa.dimen.debug.offset),
isOpen = false,
layer = layer,
)
@ -67,7 +76,9 @@ fun SceneDebugPanel(
)
scene.elements.forEach { element ->
SceneElementDebug(
modifier = Modifier.padding(start = MaterialTheme.lwa.dimen.debug.offset),
modifier = Modifier
.animateContentSize()
.padding(start = MaterialTheme.lwa.dimen.debug.offset),
isOpen = false,
element = element,
)