Initial WebAssembly Build Target
* Initial WebAssembly build target for Doodle in the wasm/ folder. * Add a new render.Engine implementation, lib/render/canvas that uses the HTML 5 Canvas API instead of SDL2 for the WebAssembly target. * Ported the basic DrawLine(), DrawBox() etc. functions from SDL2 to Canvas context2d API. * Fonts are handled with CSS embedded fonts named after the font filename and defined in wasm/index.html * `make wasm` builds the WASM program, and `make wasm-serve` runs a dev Go server that hosts the WASM file for development. The server also watches the dev tree for *.go files and rebuilds the WASM binary automatically on change. * This build "basically" runs the game. UI and fonts all work and mouse movements and clicks are detected. No wallpaper support yet or texture caching (which will crash the game as soon as you click and draw a pixel in your map!)
This commit is contained in:
parent
5e9443bcff
commit
ecffcc223d
37
canvas/canvas.go
Normal file
37
canvas/canvas.go
Normal file
|
@ -0,0 +1,37 @@
|
|||
package canvas
|
||||
|
||||
import (
|
||||
"syscall/js"
|
||||
)
|
||||
|
||||
// Canvas represents an HTML5 Canvas object.
|
||||
type Canvas struct {
|
||||
Value js.Value
|
||||
ctx2d js.Value
|
||||
}
|
||||
|
||||
// GetCanvas gets an HTML5 Canvas object from the DOM.
|
||||
func GetCanvas(id string) Canvas {
|
||||
canvasEl := js.Global().Get("document").Call("getElementById", id)
|
||||
canvas2d := canvasEl.Call("getContext", "2d")
|
||||
|
||||
c := Canvas{
|
||||
Value: canvasEl,
|
||||
ctx2d: canvas2d,
|
||||
}
|
||||
|
||||
canvasEl.Set("width", c.ClientW())
|
||||
canvasEl.Set("height", c.ClientH())
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
// ClientW returns the client width.
|
||||
func (c Canvas) ClientW() int {
|
||||
return c.Value.Get("clientWidth").Int()
|
||||
}
|
||||
|
||||
// ClientH returns the client height.
|
||||
func (c Canvas) ClientH() int {
|
||||
return c.Value.Get("clientHeight").Int()
|
||||
}
|
66
canvas/draw.go
Normal file
66
canvas/draw.go
Normal file
|
@ -0,0 +1,66 @@
|
|||
package canvas
|
||||
|
||||
import (
|
||||
"syscall/js"
|
||||
|
||||
"git.kirsle.net/apps/doodle/lib/render"
|
||||
)
|
||||
|
||||
// Methods here implement the drawing functions of the render.Engine
|
||||
|
||||
// Clear the canvas to a certain color.
|
||||
func (e *Engine) Clear(color render.Color) {
|
||||
e.canvas.ctx2d.Set("fillStyle", color.ToHex())
|
||||
e.canvas.ctx2d.Call("fillRect", 0, 0, e.width, e.height)
|
||||
}
|
||||
|
||||
// SetTitle sets the window title.
|
||||
func (e *Engine) SetTitle(title string) {
|
||||
js.Global().Get("document").Set("title", title)
|
||||
}
|
||||
|
||||
// DrawPoint draws a pixel.
|
||||
func (e *Engine) DrawPoint(color render.Color, point render.Point) {
|
||||
e.canvas.ctx2d.Set("fillStyle", color.ToHex())
|
||||
e.canvas.ctx2d.Call("fillRect",
|
||||
int(point.X),
|
||||
int(point.Y),
|
||||
1,
|
||||
1,
|
||||
)
|
||||
}
|
||||
|
||||
// DrawLine draws a line between two points.
|
||||
func (e *Engine) DrawLine(color render.Color, a, b render.Point) {
|
||||
e.canvas.ctx2d.Set("fillStyle", color.ToHex())
|
||||
for pt := range render.IterLine2(a, b) {
|
||||
e.canvas.ctx2d.Call("fillRect",
|
||||
int(pt.X),
|
||||
int(pt.Y),
|
||||
1,
|
||||
1,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// DrawRect draws a rectangle.
|
||||
func (e *Engine) DrawRect(color render.Color, rect render.Rect) {
|
||||
e.canvas.ctx2d.Set("strokeStyle", color.ToHex())
|
||||
e.canvas.ctx2d.Call("strokeRect",
|
||||
int(rect.X),
|
||||
int(rect.Y),
|
||||
int(rect.W),
|
||||
int(rect.H),
|
||||
)
|
||||
}
|
||||
|
||||
// DrawBox draws a filled rectangle.
|
||||
func (e *Engine) DrawBox(color render.Color, rect render.Rect) {
|
||||
e.canvas.ctx2d.Set("fillStyle", color.ToHex())
|
||||
e.canvas.ctx2d.Call("fillRect",
|
||||
int(rect.X),
|
||||
int(rect.Y),
|
||||
int(rect.W),
|
||||
int(rect.H),
|
||||
)
|
||||
}
|
77
canvas/engine.go
Normal file
77
canvas/engine.go
Normal file
|
@ -0,0 +1,77 @@
|
|||
package canvas
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"git.kirsle.net/apps/doodle/lib/events"
|
||||
"git.kirsle.net/apps/doodle/lib/render"
|
||||
)
|
||||
|
||||
// Engine implements a rendering engine targeting an HTML canvas for
|
||||
// WebAssembly targets.
|
||||
type Engine struct {
|
||||
canvas Canvas
|
||||
startTime time.Time
|
||||
width int
|
||||
height int
|
||||
ticks uint32
|
||||
|
||||
// Private fields.
|
||||
events *events.State
|
||||
running bool
|
||||
}
|
||||
|
||||
// New creates the Canvas Engine.
|
||||
func New(canvasID string) (*Engine, error) {
|
||||
canvas := GetCanvas(canvasID)
|
||||
|
||||
engine := &Engine{
|
||||
canvas: canvas,
|
||||
startTime: time.Now(),
|
||||
events: events.New(),
|
||||
width: canvas.ClientW(),
|
||||
height: canvas.ClientH(),
|
||||
}
|
||||
|
||||
return engine, nil
|
||||
}
|
||||
|
||||
// WindowSize returns the size of the canvas window.
|
||||
func (e *Engine) WindowSize() (w, h int) {
|
||||
return e.canvas.ClientW(), e.canvas.ClientH()
|
||||
}
|
||||
|
||||
// GetTicks returns the current tick count.
|
||||
func (e *Engine) GetTicks() uint32 {
|
||||
return e.ticks
|
||||
}
|
||||
|
||||
// TO BE IMPLEMENTED...
|
||||
|
||||
func (e *Engine) Setup() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *Engine) Present() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *Engine) NewBitmap(filename string) (render.Texturer, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (e *Engine) Copy(t render.Texturer, src, dist render.Rect) {
|
||||
|
||||
}
|
||||
|
||||
// Delay for a moment.
|
||||
func (e *Engine) Delay(delay uint32) {
|
||||
time.Sleep(time.Duration(delay) * time.Millisecond)
|
||||
}
|
||||
|
||||
// Teardown tasks.
|
||||
func (e *Engine) Teardown() {}
|
||||
|
||||
func (e *Engine) Loop() error {
|
||||
return nil
|
||||
}
|
102
canvas/events.go
Normal file
102
canvas/events.go
Normal file
|
@ -0,0 +1,102 @@
|
|||
package canvas
|
||||
|
||||
import (
|
||||
"syscall/js"
|
||||
|
||||
"git.kirsle.net/apps/doodle/lib/events"
|
||||
"git.kirsle.net/apps/doodle/pkg/log"
|
||||
)
|
||||
|
||||
// AddEventListeners sets up bindings to collect events from the browser.
|
||||
func (e *Engine) AddEventListeners() {
|
||||
s := e.events
|
||||
|
||||
// Mouse movement.
|
||||
e.canvas.Value.Call(
|
||||
"addEventListener",
|
||||
"mousemove",
|
||||
js.FuncOf(func(this js.Value, args []js.Value) interface{} {
|
||||
var (
|
||||
x = args[0].Get("pageX").Int()
|
||||
y = args[0].Get("pageY").Int()
|
||||
)
|
||||
|
||||
s.CursorX.Push(int32(x))
|
||||
s.CursorY.Push(int32(y))
|
||||
return nil
|
||||
}),
|
||||
)
|
||||
|
||||
// Mouse clicks.
|
||||
for _, ev := range []string{"mouseup", "mousedown"} {
|
||||
ev := ev
|
||||
e.canvas.Value.Call(
|
||||
"addEventListener",
|
||||
ev,
|
||||
js.FuncOf(func(this js.Value, args []js.Value) interface{} {
|
||||
var (
|
||||
x = args[0].Get("pageX").Int()
|
||||
y = args[0].Get("pageY").Int()
|
||||
which = args[0].Get("which").Int()
|
||||
)
|
||||
|
||||
log.Info("Clicked at %d,%d", x, y)
|
||||
|
||||
s.CursorX.Push(int32(x))
|
||||
s.CursorY.Push(int32(y))
|
||||
|
||||
// Is a mouse button pressed down?
|
||||
checkDown := func(number int) bool {
|
||||
if which == number {
|
||||
return ev == "mousedown"
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
s.Button1.Push(checkDown(1))
|
||||
s.Button2.Push(checkDown(3))
|
||||
return false
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
// Supress context menu.
|
||||
e.canvas.Value.Call(
|
||||
"addEventListener",
|
||||
"contextmenu",
|
||||
js.FuncOf(func(this js.Value, args []js.Value) interface{} {
|
||||
args[0].Call("preventDefault")
|
||||
return false
|
||||
}),
|
||||
)
|
||||
|
||||
// Keyboard keys
|
||||
// js.Global().Get("document").Call(
|
||||
// "addEventListener",
|
||||
// "keydown",
|
||||
// js.FuncOf(func(this js.Value, args []js.Value) interface{} {
|
||||
// log.Info("key: %+v", args)
|
||||
// var (
|
||||
// event = args[0]
|
||||
// charCode = event.Get("charCode")
|
||||
// key = event.Get("key").String()
|
||||
// )
|
||||
//
|
||||
// switch key {
|
||||
// case "Enter":
|
||||
// s.EnterKey.Push(true)
|
||||
// // default:
|
||||
// // s.KeyName.Push(key)
|
||||
// }
|
||||
//
|
||||
// log.Info("keypress: code=%s key=%s", charCode, key)
|
||||
//
|
||||
// return nil
|
||||
// }),
|
||||
// )
|
||||
}
|
||||
|
||||
// Poll for events.
|
||||
func (e *Engine) Poll() (*events.State, error) {
|
||||
return e.events, nil
|
||||
}
|
88
canvas/text.go
Normal file
88
canvas/text.go
Normal file
|
@ -0,0 +1,88 @@
|
|||
package canvas
|
||||
|
||||
// Text rendering functions using the HTML 5 canvas.
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"git.kirsle.net/apps/doodle/lib/render"
|
||||
)
|
||||
|
||||
// FontFilenameToName converts a FontFilename to its CSS font name.
|
||||
//
|
||||
// The CSS font name is set to the base of the filename, without the .ttf
|
||||
// file extension. For example, "fonts/DejaVuSans.ttf" uses the CSS font
|
||||
// family name "DejaVuSans" and that's what this function returns.
|
||||
//
|
||||
// Fonts must be defined in the index.html style sheet when serving the
|
||||
// wasm build of Doodle.
|
||||
//
|
||||
// If filename is "", returns "serif" as a sensible default.
|
||||
func FontFilenameToName(filename string) string {
|
||||
if filename == "" {
|
||||
return "DejaVuSans,serif"
|
||||
}
|
||||
return strings.TrimSuffix(filepath.Base(filename), filepath.Ext(filename))
|
||||
}
|
||||
|
||||
// DrawText draws text on the canvas.
|
||||
func (e *Engine) DrawText(text render.Text, point render.Point) error {
|
||||
font := FontFilenameToName(text.FontFilename)
|
||||
e.canvas.ctx2d.Set("font",
|
||||
fmt.Sprintf("%dpx %s,serif", text.Size, font),
|
||||
)
|
||||
|
||||
e.canvas.ctx2d.Set("textBaseline", "top")
|
||||
|
||||
write := func(dx, dy int, color render.Color) {
|
||||
e.canvas.ctx2d.Set("fillStyle", color.ToHex())
|
||||
e.canvas.ctx2d.Call("fillText",
|
||||
text.Text,
|
||||
int(point.X)+dx,
|
||||
int(point.Y)+dy,
|
||||
)
|
||||
}
|
||||
|
||||
// Does the text have a stroke around it?
|
||||
if text.Stroke != render.Invisible {
|
||||
e.canvas.ctx2d.Set("fillStyle", text.Stroke.ToHex())
|
||||
write(-1, -1, text.Stroke)
|
||||
write(-1, 0, text.Stroke)
|
||||
write(-1, 1, text.Stroke)
|
||||
write(1, -1, text.Stroke)
|
||||
write(1, 0, text.Stroke)
|
||||
write(1, 1, text.Stroke)
|
||||
write(0, -1, text.Stroke)
|
||||
write(0, 1, text.Stroke)
|
||||
}
|
||||
|
||||
// Does it have a drop shadow?
|
||||
if text.Shadow != render.Invisible {
|
||||
write(1, 1, text.Shadow)
|
||||
}
|
||||
|
||||
// Draw the text itself.
|
||||
write(0, 0, text.Color)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ComputeTextRect computes and returns a Rect for how large the text would
|
||||
// appear if rendered.
|
||||
func (e *Engine) ComputeTextRect(text render.Text) (render.Rect, error) {
|
||||
font := FontFilenameToName(text.FontFilename)
|
||||
e.canvas.ctx2d.Set("font",
|
||||
fmt.Sprintf("%dpx %s,serif", text.Size, font),
|
||||
)
|
||||
|
||||
measure := e.canvas.ctx2d.Call("measureText", text.Text)
|
||||
rect := render.Rect{
|
||||
// TODO: the only TextMetrics widely supported in browsers is
|
||||
// the width. For height, use the text size for now.
|
||||
W: int32(measure.Get("width").Int()),
|
||||
H: int32(text.Size),
|
||||
}
|
||||
return rect, nil
|
||||
}
|
8
color.go
8
color.go
|
@ -113,6 +113,14 @@ func (c Color) String() string {
|
|||
)
|
||||
}
|
||||
|
||||
// ToHex converts a render.Color to standard #RRGGBB hexadecimal format.
|
||||
func (c Color) ToHex() string {
|
||||
return fmt.Sprintf(
|
||||
"#%02x%02x%02x",
|
||||
c.Red, c.Green, c.Blue,
|
||||
)
|
||||
}
|
||||
|
||||
// ToColor converts a render.Color into a Go standard color.Color
|
||||
func (c Color) ToColor() color.RGBA {
|
||||
return color.RGBA{
|
||||
|
|
Loading…
Reference in New Issue
Block a user