Skip to content

Commit 79cca1c

Browse files
committed
Bug 2022850 - add game canvas and draw code.
This handles head, tail, body and food being drawn onto the canvas. Also includes the assets.
1 parent 8d79e46 commit 79cca1c

9 files changed

Lines changed: 744 additions & 0 deletions

File tree

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
* This Source Code Form is subject to the terms of the Mozilla Public
3+
* License, v. 2.0. If a copy of the MPL was not distributed with this
4+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
5+
*/
6+
7+
package org.mozilla.fenix.longfox
8+
9+
import androidx.compose.ui.geometry.Offset
10+
import androidx.compose.ui.graphics.ImageBitmap
11+
import androidx.compose.ui.graphics.drawscope.DrawScope
12+
13+
/**
14+
* Draw the food onto the canvas at the current food grid point.
15+
*
16+
* @receiver the draw scope for the game canvas
17+
* @param state the current game state
18+
* @param food the food bitmap
19+
*/
20+
fun DrawScope.drawFood(state: GameState, food: ImageBitmap?) {
21+
if (food == null) return
22+
drawImage(
23+
image = food,
24+
topLeft = Offset(state.food.x * state.cellSize, state.food.y * state.cellSize),
25+
)
26+
}
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
/*
2+
* This Source Code Form is subject to the terms of the Mozilla Public
3+
* License, v. 2.0. If a copy of the MPL was not distributed with this
4+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
5+
*/
6+
7+
package org.mozilla.fenix.longfox
8+
9+
import androidx.compose.ui.geometry.CornerRadius
10+
import androidx.compose.ui.geometry.Offset
11+
import androidx.compose.ui.geometry.Rect
12+
import androidx.compose.ui.geometry.RoundRect
13+
import androidx.compose.ui.geometry.Size
14+
import androidx.compose.ui.graphics.Brush
15+
import androidx.compose.ui.graphics.Color
16+
import androidx.compose.ui.graphics.Path
17+
import androidx.compose.ui.graphics.drawscope.DrawScope
18+
19+
/**
20+
* Draw the body of the fox at the position given by the current game state.
21+
* We pass in some path objects because they're a bit expensive and we want this code to be fast.
22+
* The shoulders and the... square before the tail are special because they have rounded corners
23+
* to make the fox cuter
24+
*
25+
* @receiver the draw scope for the game canvas
26+
* @param state the current game state
27+
* @param shouldersPath the fox's shoulders
28+
* @param bottomPath the fox's bottom
29+
*/
30+
fun DrawScope.drawBody(state: GameState, shouldersPath: Path, bottomPath: Path) {
31+
val brush = Brush.linearGradient(listOf(Color.Red, Color.Yellow))
32+
val foxBody = state.fox.drop(1).dropLast(1)
33+
val cornerRadiusPx = state.cellSize / 2
34+
val cornerRadius = CornerRadius(cornerRadiusPx, cornerRadiusPx)
35+
when (foxBody.size) {
36+
1 -> {
37+
drawRoundRect(
38+
brush = brush,
39+
cornerRadius = cornerRadius,
40+
topLeft = Offset(
41+
foxBody.first().x * state.cellSize,
42+
foxBody.first().y * state.cellSize,
43+
),
44+
size = Size(state.cellSize, state.cellSize),
45+
)
46+
}
47+
48+
else -> {
49+
val shoulders = foxBody.first()
50+
shouldersPath.apply {
51+
reset()
52+
addRoundRect(
53+
RoundRect(
54+
rect = Rect(
55+
offset = Offset(
56+
shoulders.x * state.cellSize,
57+
shoulders.y * state.cellSize,
58+
),
59+
size = Size(state.cellSize, state.cellSize),
60+
),
61+
topLeft = when (state.shouldersDirection) {
62+
Direction.UP, Direction.LEFT -> cornerRadius
63+
Direction.DOWN, Direction.RIGHT -> CornerRadius.Zero
64+
},
65+
topRight = when (state.shouldersDirection) {
66+
Direction.UP, Direction.RIGHT -> cornerRadius
67+
Direction.DOWN, Direction.LEFT -> CornerRadius.Zero
68+
},
69+
bottomLeft = when (state.shouldersDirection) {
70+
Direction.DOWN, Direction.LEFT -> cornerRadius
71+
Direction.UP, Direction.RIGHT -> CornerRadius.Zero
72+
},
73+
bottomRight = when (state.shouldersDirection) {
74+
Direction.DOWN, Direction.RIGHT -> cornerRadius
75+
Direction.UP, Direction.LEFT -> CornerRadius.Zero
76+
},
77+
),
78+
)
79+
}
80+
drawPath(shouldersPath, brush)
81+
82+
foxBody.drop(1).dropLast(1).forEach { (x, y) ->
83+
drawRect(
84+
brush = brush,
85+
topLeft = Offset(x * state.cellSize, y * state.cellSize),
86+
size = Size(state.cellSize, state.cellSize),
87+
)
88+
}
89+
90+
val bottom = foxBody.last()
91+
bottomPath.apply {
92+
reset()
93+
addRoundRect(
94+
RoundRect(
95+
rect = Rect(
96+
offset = Offset(bottom.x * state.cellSize, bottom.y * state.cellSize),
97+
size = Size(state.cellSize, state.cellSize),
98+
),
99+
topLeft = when (state.tailDirection) {
100+
Direction.UP, Direction.LEFT -> cornerRadius
101+
Direction.DOWN, Direction.RIGHT -> CornerRadius.Zero
102+
},
103+
topRight = when (state.tailDirection) {
104+
Direction.UP, Direction.RIGHT -> cornerRadius
105+
Direction.DOWN, Direction.LEFT -> CornerRadius.Zero
106+
},
107+
bottomLeft = when (state.tailDirection) {
108+
Direction.DOWN, Direction.LEFT -> cornerRadius
109+
Direction.UP, Direction.RIGHT -> CornerRadius.Zero
110+
},
111+
bottomRight = when (state.tailDirection) {
112+
Direction.DOWN, Direction.RIGHT -> cornerRadius
113+
Direction.UP, Direction.LEFT -> CornerRadius.Zero
114+
},
115+
),
116+
)
117+
}
118+
drawPath(bottomPath, brush)
119+
}
120+
}
121+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
2+
* This Source Code Form is subject to the terms of the Mozilla Public
3+
* License, v. 2.0. If a copy of the MPL was not distributed with this
4+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
5+
*/
6+
7+
package org.mozilla.fenix.longfox
8+
9+
import androidx.compose.ui.geometry.Offset
10+
import androidx.compose.ui.graphics.ImageBitmap
11+
import androidx.compose.ui.graphics.drawscope.DrawScope
12+
13+
/**
14+
* Draw the head of the fox at the position given by the current game state.
15+
*
16+
* @receiver the draw scope for the game canvas
17+
* @param state the current game state
18+
* @param kitHeadBitmap the fox head bitmap
19+
*/
20+
fun DrawScope.drawHead(state: GameState, kitHeadBitmap: ImageBitmap?) {
21+
if (kitHeadBitmap == null) return
22+
val head = state.fox.first()
23+
drawImage(
24+
image = kitHeadBitmap,
25+
topLeft = Offset(head.x * state.cellSize, head.y * state.cellSize),
26+
)
27+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* This Source Code Form is subject to the terms of the Mozilla Public
3+
* License, v. 2.0. If a copy of the MPL was not distributed with this
4+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
5+
*/
6+
7+
package org.mozilla.fenix.longfox
8+
9+
import androidx.compose.ui.geometry.Offset
10+
import androidx.compose.ui.graphics.ImageBitmap
11+
import androidx.compose.ui.graphics.drawscope.DrawScope
12+
import androidx.compose.ui.graphics.drawscope.rotate
13+
14+
/**
15+
* Draw the tail of the fox at the position given by the current game state.
16+
* The tail is rotated to match the direction that part of the fox is moving.
17+
* So it does a little swoosh as it turns :)
18+
*
19+
* @receiver the draw scope for the game canvas
20+
* @param state the current game state
21+
* @param kitTailBitmap the fox tail bitmap
22+
*/
23+
fun DrawScope.drawTail(state: GameState, kitTailBitmap: ImageBitmap?) {
24+
if (kitTailBitmap == null) return
25+
val tail = state.fox.last()
26+
val rotateAngle = when (state.tailDirection) {
27+
Direction.UP -> 0F
28+
Direction.DOWN -> 180F
29+
Direction.LEFT -> 270F
30+
Direction.RIGHT -> 90F
31+
}
32+
val topLeft = Offset(tail.x * state.cellSize, tail.y * state.cellSize)
33+
val pivotPoint = Offset(topLeft.x + state.cellSize / 2, topLeft.y + state.cellSize / 2)
34+
rotate(rotateAngle, pivotPoint) {
35+
drawImage(
36+
image = kitTailBitmap,
37+
topLeft = topLeft,
38+
)
39+
}
40+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/*
2+
* This Source Code Form is subject to the terms of the Mozilla Public
3+
* License, v. 2.0. If a copy of the MPL was not distributed with this
4+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
5+
*/
6+
7+
package org.mozilla.fenix.longfox
8+
9+
import androidx.compose.foundation.Canvas
10+
import androidx.compose.foundation.background
11+
import androidx.compose.foundation.layout.size
12+
import androidx.compose.material3.MaterialTheme
13+
import androidx.compose.runtime.Composable
14+
import androidx.compose.runtime.remember
15+
import androidx.compose.ui.Modifier
16+
import androidx.compose.ui.geometry.Size
17+
import androidx.compose.ui.graphics.Color
18+
import androidx.compose.ui.graphics.Path
19+
import androidx.compose.ui.graphics.asImageBitmap
20+
import androidx.compose.ui.platform.LocalContext
21+
import androidx.compose.ui.tooling.preview.Preview
22+
import androidx.compose.ui.unit.dp
23+
import androidx.core.content.ContextCompat
24+
import androidx.core.graphics.drawable.toBitmap
25+
import org.mozilla.fenix.longfox.GameState.Companion.CELL_SIZE_DP
26+
27+
/**
28+
* A canvas that draws the fox game.
29+
* The canvas is sized to fit the container, with a fixed cell size.
30+
* It gets all the required drawables and resizes them to square bitmaps that match the cell size.
31+
* Then it passes the assets into the separately defined drawscope functions for the fox's
32+
* body, head, tail and food.
33+
*
34+
* @param state the current game state
35+
* @param onSize a callback that is called when the canvas is resized
36+
*/
37+
@Composable
38+
fun GameCanvas(state: GameState, onSize: (Size) -> Unit) {
39+
val context = LocalContext.current
40+
val cellSize = state.cellSize.toInt()
41+
42+
val kitHead = remember(cellSize) {
43+
if (cellSize > 0) {
44+
ContextCompat.getDrawable(context, R.drawable.kit_head)
45+
?.toBitmap(cellSize, cellSize)
46+
?.asImageBitmap()
47+
} else {
48+
null
49+
}
50+
}
51+
52+
val kitTail = remember(cellSize) {
53+
if (cellSize > 0) {
54+
ContextCompat.getDrawable(context, R.drawable.kit_tail)
55+
?.toBitmap(cellSize, cellSize)
56+
?.asImageBitmap()
57+
} else {
58+
null
59+
}
60+
}
61+
62+
val cookie = remember(cellSize) {
63+
if (cellSize > 0) {
64+
ContextCompat.getDrawable(context, R.drawable.cookie)
65+
?.toBitmap(cellSize, cellSize)
66+
?.asImageBitmap()
67+
} else {
68+
null
69+
}
70+
}
71+
72+
val shouldersPath = remember { Path() }
73+
val bottomPath = remember { Path() }
74+
75+
Canvas(
76+
modifier = Modifier
77+
.background(color = Color.Black)
78+
.size((CELL_SIZE_DP * state.numCellsWide).dp),
79+
) {
80+
onSize(size)
81+
drawHead(state, kitHead)
82+
drawBody(state, shouldersPath, bottomPath)
83+
drawTail(state, kitTail)
84+
drawFood(state, cookie)
85+
}
86+
}
87+
88+
@Preview
89+
@Composable
90+
fun GameCanvasPreview() {
91+
MaterialTheme {
92+
GameCanvas(GameState(size = Size(600f, 1000f)), onSize = { _ -> })
93+
}
94+
}

mobile/android/fenix/app/longfox/src/main/kotlin/org/mozilla/fenix/longfox/GameState.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ data class GameState(
4343
val numCells: Int = 12,
4444
) {
4545

46+
companion object {
47+
const val CELL_SIZE_DP = 20f
48+
}
49+
4650
val numCellsWide = numCells
4751
val numCellsTall = numCellsWide
4852
val cellSize = (size.minDimension / numCellsWide).toInt().toFloat()
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<!--
2+
~ This Source Code Form is subject to the terms of the Mozilla Public
3+
~ License, v. 2.0. If a copy of the MPL was not distributed with this
4+
~ file, You can obtain one at http://mozilla.org/MPL/2.0/.
5+
-->
6+
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#EAB52E" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
7+
8+
<path android:fillColor="@android:color/white" android:pathData="M21.95,10.99c-1.79,-0.03 -3.7,-1.95 -2.68,-4.22c-2.98,1 -5.77,-1.59 -5.19,-4.56C6.95,0.71 2,6.58 2,12c0,5.52 4.48,10 10,10C17.89,22 22.54,16.92 21.95,10.99zM8.5,15C7.67,15 7,14.33 7,13.5S7.67,12 8.5,12s1.5,0.67 1.5,1.5S9.33,15 8.5,15zM10.5,10C9.67,10 9,9.33 9,8.5S9.67,7 10.5,7S12,7.67 12,8.5S11.33,10 10.5,10zM15,16c-0.55,0 -1,-0.45 -1,-1c0,-0.55 0.45,-1 1,-1s1,0.45 1,1C16,15.55 15.55,16 15,16z"/>
9+
10+
</vector>

0 commit comments

Comments
 (0)