Skip to content

Commit ba40790

Browse files
committed
feat: game draw APIs
1 parent 6492a55 commit ba40790

File tree

8 files changed

+385
-35
lines changed

8 files changed

+385
-35
lines changed

docs/articles/api/game-draw.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# game draw
2+
3+
Declarative helpers for issuing 2D canvas drawing commands.
4+
5+
| Function | Description |
6+
| --- | --- |
7+
| `NewCanvas(el dom.Element) (Canvas, bool)` | Acquire a 2D rendering surface from a `<canvas>` element. |
8+
| `(Canvas).Valid() bool` | Report whether the canvas has an active 2D context. |
9+
| `(Canvas).SetSize(width, height float64)` | Update the backing canvas width and height in pixels. |
10+
| `(Canvas).Draw(cmds ...Command)` | Execute the provided drawing commands, skipping `nil` entries. |
11+
| `Rectangle(x, y, width, height float64) *Rect` | Create a rectangle command for the given bounds. |
12+
| `(*Rect).Fill(color string) *Rect` | Set the rectangle fill color. |
13+
| `(*Rect).Stroke(color string, width float64) *Rect` | Set the rectangle stroke color and width. |
14+
| `Disc(x, y, radius float64) *Circle` | Create a circle command centered at the provided coordinates. |
15+
| `(*Circle).Fill(color string) *Circle` | Set the circle fill color. |
16+
| `(*Circle).Stroke(color string, width float64) *Circle` | Set the circle stroke color and width. |
17+
| `Segment(x1, y1, x2, y2 float64) *Line` | Create a line segment command between the two points. |
18+
| `(*Line).Stroke(color string, width float64) *Line` | Set the line stroke color and width. |

docs/articles/api/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ The full set of API references is listed below:
6464
- [webgl](webgl)
6565
- [math](math)
6666
- [game loop](game-loop)
67+
- [game draw](game-draw)
6768
- [scene](scene)
6869
- [netcode](netcode)
6970
- [pathfinding](pathfinding)

docs/articles/sidebar.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,10 @@
338338
"title": "game loop",
339339
"path": "api/game-loop.md"
340340
},
341+
{
342+
"title": "game draw",
343+
"path": "api/game-draw.md"
344+
},
341345
{
342346
"title": "scene",
343347
"path": "api/scene.md"

docs/examples/components/multiplayer_component.go

Lines changed: 36 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
core "github.com/rfwlab/rfw/v1/core"
1515
dom "github.com/rfwlab/rfw/v1/dom"
1616
events "github.com/rfwlab/rfw/v1/events"
17+
draw "github.com/rfwlab/rfw/v1/game/draw"
1718
hostclient "github.com/rfwlab/rfw/v1/hostclient"
1819
js "github.com/rfwlab/rfw/v1/js"
1920
"github.com/rfwlab/rfw/v1/netcode"
@@ -62,8 +63,7 @@ type multiplayerComponent struct {
6263
cancelFuncs []func()
6364
stop chan struct{}
6465
sessionID string
65-
canvas js.Value
66-
ctx js.Value
66+
surface draw.Canvas
6767
}
6868

6969
// NewMultiplayerComponent renders the multiplayer arena example page.
@@ -82,11 +82,12 @@ func (c *multiplayerComponent) OnMount() {
8282
doc := dom.Doc()
8383
canvas := doc.ByID("mp-canvas")
8484
if !canvas.IsNull() && !canvas.IsUndefined() {
85-
canvas.Set("width", int(arenaWidth))
86-
canvas.Set("height", int(arenaHeight))
87-
c.canvas = canvas.Value
88-
c.ctx = canvas.Call("getContext", "2d")
89-
c.ctx.Set("lineWidth", 2)
85+
if surface, ok := draw.NewCanvas(canvas); ok {
86+
surface.SetSize(arenaWidth, arenaHeight)
87+
c.surface = surface
88+
} else {
89+
c.surface = draw.Canvas{}
90+
}
9091
}
9192

9293
c.cancelFuncs = []func(){
@@ -109,6 +110,7 @@ func (c *multiplayerComponent) OnUnmount() {
109110
}
110111
c.cancelFuncs = nil
111112
c.client = nil
113+
c.surface = draw.Canvas{}
112114
}
113115

114116
func (c *multiplayerComponent) loop() {
@@ -222,39 +224,33 @@ func (c *multiplayerComponent) consumeShoot() bool {
222224
}
223225

224226
func (c *multiplayerComponent) render(state mpSnapshot) {
225-
if !c.ctx.Truthy() {
227+
if !c.surface.Valid() {
226228
return
227229
}
228-
c.ctx.Set("fillStyle", "#0f172a")
229-
c.ctx.Call("fillRect", 0, 0, arenaWidth, arenaHeight)
230230

231-
c.ctx.Set("fillStyle", "#f8fafc")
231+
commands := make([]draw.Command, 0, 1+len(state.Bullets)+len(state.Players)*3)
232+
commands = append(commands, draw.Rectangle(0, 0, arenaWidth, arenaHeight).Fill(arenaBackground))
233+
232234
for _, bullet := range state.Bullets {
233-
c.ctx.Call("beginPath")
234-
c.ctx.Call("arc", bullet.X, bullet.Y, bulletRadius, 0, math.Pi*2, false)
235-
c.ctx.Call("fill")
235+
commands = append(commands, draw.Disc(bullet.X, bullet.Y, bulletRadius).Fill(bulletColor))
236236
}
237237

238238
for id, player := range state.Players {
239-
c.ctx.Set("fillStyle", player.Color)
240-
c.ctx.Call("beginPath")
241-
c.ctx.Call("arc", player.X, player.Y, playerRadius, 0, math.Pi*2, false)
242-
c.ctx.Call("fill")
239+
marker := draw.Disc(player.X, player.Y, playerRadius).Fill(player.Color)
243240
if id == c.sessionID {
244-
c.ctx.Set("strokeStyle", "#ffffff")
245-
c.ctx.Call("stroke")
241+
marker.Stroke(activePlayerStroke, outlineWidth)
246242
}
243+
commands = append(commands, marker)
247244
if !player.Alive {
248-
c.ctx.Set("strokeStyle", "rgba(15, 23, 42, 0.7)")
249-
c.ctx.Call("beginPath")
250-
c.ctx.Call("moveTo", player.X-playerRadius, player.Y-playerRadius)
251-
c.ctx.Call("lineTo", player.X+playerRadius, player.Y+playerRadius)
252-
c.ctx.Call("moveTo", player.X-playerRadius, player.Y+playerRadius)
253-
c.ctx.Call("lineTo", player.X+playerRadius, player.Y-playerRadius)
254-
c.ctx.Call("stroke")
245+
commands = append(commands,
246+
draw.Segment(player.X-playerRadius, player.Y-playerRadius, player.X+playerRadius, player.Y+playerRadius).Stroke(eliminatedStroke, outlineWidth),
247+
draw.Segment(player.X-playerRadius, player.Y+playerRadius, player.X+playerRadius, player.Y-playerRadius).Stroke(eliminatedStroke, outlineWidth),
248+
)
255249
}
256250
}
257251

252+
c.surface.Draw(commands...)
253+
258254
c.updateHUD(state)
259255
}
260256

@@ -301,14 +297,14 @@ func (c *multiplayerComponent) updateHUD(state mpSnapshot) {
301297
message := ""
302298
if state.Winner != "" {
303299
if state.Winner == c.sessionID {
304-
message = "Hai vinto! Ultimo in piedi."
300+
message = "You won! Last player standing."
305301
} else if winner, ok := state.Players[state.Winner]; ok {
306-
message = fmt.Sprintf("Vince il giocatore %s", shortID(winner.ID))
302+
message = fmt.Sprintf("Player %s wins", shortID(winner.ID))
307303
} else {
308-
message = "Partita conclusa."
304+
message = "Match finished."
309305
}
310306
} else if player, ok := state.Players[c.sessionID]; ok && !player.Alive {
311-
message = "Game over! Premi WASD per muoverti quando inizia un nuovo round."
307+
message = "Game over! Press WASD to move when the next round starts."
312308
}
313309
statusEl.SetText(message)
314310
}
@@ -330,8 +326,13 @@ func shortID(id string) string {
330326
}
331327

332328
const (
333-
bulletRadius = 6.0
334-
playerRadius = 18.0
335-
arenaWidth = 800.0
336-
arenaHeight = 520.0
329+
bulletRadius = 6.0
330+
playerRadius = 18.0
331+
arenaWidth = 800.0
332+
arenaHeight = 520.0
333+
outlineWidth = 2.0
334+
arenaBackground = "#0f172a"
335+
bulletColor = "#f8fafc"
336+
activePlayerStroke = "#ffffff"
337+
eliminatedStroke = "rgba(15, 23, 42, 0.7)"
337338
)

v1/game/draw/canvas.go

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
package draw
2+
3+
// Canvas represents a 2D drawing surface built on top of a canvas element.
4+
type Canvas struct {
5+
impl canvasImpl
6+
}
7+
8+
type canvasImpl interface {
9+
valid() bool
10+
setSize(width, height float64)
11+
drawRect(Rect)
12+
drawCircle(Circle)
13+
drawLine(Line)
14+
}
15+
16+
// Valid reports whether the canvas is ready for drawing commands.
17+
func (c Canvas) Valid() bool {
18+
if c.impl == nil {
19+
return false
20+
}
21+
return c.impl.valid()
22+
}
23+
24+
// SetSize updates the backing canvas dimensions.
25+
func (c Canvas) SetSize(width, height float64) {
26+
if c.impl == nil {
27+
return
28+
}
29+
c.impl.setSize(width, height)
30+
}
31+
32+
// Draw executes the provided commands in order when the canvas is valid.
33+
func (c Canvas) Draw(cmds ...Command) {
34+
if !c.Valid() {
35+
return
36+
}
37+
for _, cmd := range cmds {
38+
if cmd != nil {
39+
cmd.draw(c)
40+
}
41+
}
42+
}
43+
44+
func (c Canvas) drawRect(r Rect) {
45+
if c.impl == nil {
46+
return
47+
}
48+
c.impl.drawRect(r)
49+
}
50+
51+
func (c Canvas) drawCircle(circle Circle) {
52+
if c.impl == nil {
53+
return
54+
}
55+
c.impl.drawCircle(circle)
56+
}
57+
58+
func (c Canvas) drawLine(line Line) {
59+
if c.impl == nil {
60+
return
61+
}
62+
c.impl.drawLine(line)
63+
}
64+
65+
// Command represents a draw instruction that can be executed on a Canvas.
66+
type Command interface {
67+
draw(Canvas)
68+
}
69+
70+
// Rect describes a rectangle drawing command.
71+
type Rect struct {
72+
X, Y float64
73+
Width, Height float64
74+
fillColor string
75+
strokeColor string
76+
strokeWidth float64
77+
}
78+
79+
// Rectangle returns a rectangle command builder for the given bounds.
80+
func Rectangle(x, y, width, height float64) *Rect {
81+
return &Rect{X: x, Y: y, Width: width, Height: height}
82+
}
83+
84+
// Fill configures the fill color for the rectangle.
85+
func (r *Rect) Fill(color string) *Rect {
86+
r.fillColor = color
87+
return r
88+
}
89+
90+
// Stroke configures the stroke color and width for the rectangle outline.
91+
func (r *Rect) Stroke(color string, width float64) *Rect {
92+
r.strokeColor = color
93+
r.strokeWidth = width
94+
return r
95+
}
96+
97+
func (r *Rect) draw(c Canvas) {
98+
c.drawRect(*r)
99+
}
100+
101+
// Circle describes a circle drawing command.
102+
type Circle struct {
103+
X, Y float64
104+
Radius float64
105+
fillColor string
106+
strokeColor string
107+
strokeWidth float64
108+
}
109+
110+
// Disc returns a circle command builder centered at the provided coordinates.
111+
func Disc(x, y, radius float64) *Circle {
112+
return &Circle{X: x, Y: y, Radius: radius}
113+
}
114+
115+
// Fill configures the fill color for the circle.
116+
func (c *Circle) Fill(color string) *Circle {
117+
c.fillColor = color
118+
return c
119+
}
120+
121+
// Stroke configures the stroke color and width for the circle outline.
122+
func (c *Circle) Stroke(color string, width float64) *Circle {
123+
c.strokeColor = color
124+
c.strokeWidth = width
125+
return c
126+
}
127+
128+
func (c *Circle) draw(canvas Canvas) {
129+
canvas.drawCircle(*c)
130+
}
131+
132+
// Line describes a line segment drawing command.
133+
type Line struct {
134+
X1, Y1 float64
135+
X2, Y2 float64
136+
strokeColor string
137+
strokeWidth float64
138+
}
139+
140+
// Segment returns a line command builder for the provided endpoints.
141+
func Segment(x1, y1, x2, y2 float64) *Line {
142+
return &Line{X1: x1, Y1: y1, X2: x2, Y2: y2}
143+
}
144+
145+
// Stroke configures the stroke color and width for the line.
146+
func (l *Line) Stroke(color string, width float64) *Line {
147+
l.strokeColor = color
148+
l.strokeWidth = width
149+
return l
150+
}
151+
152+
func (l *Line) draw(c Canvas) {
153+
c.drawLine(*l)
154+
}

0 commit comments

Comments
 (0)