Skip to content
/ canvas Public

Draw on an HTML 2D canvas in a web browser from a server program using WebSockets.

License

Notifications You must be signed in to change notification settings

fzipp/canvas

Repository files navigation

canvas

PkgGoDev Build Status Go Report Card

This Go module utilizes WebSockets to establish communication with a 2D canvas graphics context in a web browser, providing a portable way to create interactive 2D graphics from within a Go program.

The Go program (server) sends draw commands to the web browser (client) via WebSocket using a binary format. In return, the client sends keyboard, mouse, and touch events to the server.

This module does not rely on operating system-specific backends or Cgo bindings. It also does not utilize WebAssembly, which means the Go code runs on the server side, rather than in the browser. The client-server design enables the canvas to be displayed on a different machine over the network.

Examples

The example subdirectory contains a variety of demo programs.

Screenshots of examples

Usage

Drawing

The ListenAndServe function initializes the canvas server and takes the following arguments: the network address with the port number to bind to, a run function, and an options structure that configures various aspects such as the canvas size in pixels or a title for the browser tab.

The run function is called when a client connects to the server. This serves as the entry point for drawing.

package main

import (
	"image/color"
	"log"

	"github.com/fzipp/canvas"
)

func main() {
	err := canvas.ListenAndServe(":8080", run, &canvas.Options{
		Title:  "Example 1: Drawing",
		Width:  100,
		Height: 80,
	})
	if err != nil {
		log.Fatal(err)
	}
}

func run(ctx *canvas.Context) {
	ctx.SetFillStyle(color.RGBA{R: 200, A: 255})
	ctx.FillRect(10, 10, 50, 50)
	// ...
	ctx.Flush()
}

After starting the program, you can access the canvas by opening http://localhost:8080 in a web browser.

The server doesn't immediately send each drawing operation to the client but instead buffers them until the Flush method is called. The flush should occur once the image or an animation frame is complete; otherwise, nothing will be displayed.

Each client connection starts its own run function as a goroutine. Access to shared state between client connections must be synchronized. If you don't want to share state between connections, you should keep it local to the run function and pass the state to other functions called by the run function.

An animation loop

To create an animation, you can use a for loop within the run function. Inside this loop, observe the ctx.Events() channel for a canvas.CloseEvent to exit the loop when the connection is closed.

A useful pattern is to create a struct that holds the animation state and has both an update and a draw method:

package main

import (
	"log"
	"time"

	"github.com/fzipp/canvas"
)

func main() {
	err := canvas.ListenAndServe(":8080", run, &canvas.Options{
		Title:  "Example 2: Animation",
		Width:  800,
		Height: 600,
	})
	if err != nil {
		log.Fatal(err)
	}
}

func run(ctx *canvas.Context) {
	d := &demo{}
	for {
		select {
		case event := <-ctx.Events():
			if _, ok := event.(canvas.CloseEvent); ok {
				return
			}
		default:
			d.update()
			d.draw(ctx)
			ctx.Flush()
			time.Sleep(time.Second / 6)
		}
	}
}

type demo struct {
	// Animation state, for example:
	x, y int
	// ...
}

func (d *demo) update() {
	// Update animation state for the next frame
	// ...
}

func (d *demo) draw(ctx *canvas.Context) {
	// Draw the frame here, based on the animation state
	// ...
}

Keyboard, mouse and touch events

To handle keyboard, mouse, and touch events, you need to specify which events the client should observe and send to the server. This is achieved by passing an EnabledEvents option to the ListenAndServe function. Mouse move events typically generate more WebSocket communication than the others, so you may want to enable them only if necessary.

The ctx.Events() channel receives the observed events, and a type switch is used to determine the specific event type. A useful pattern involves creating a handle method for event handling:

package main

import (
	"log"

	"github.com/fzipp/canvas"
)

func main() {
	err := canvas.ListenAndServe(":8080", run, &canvas.Options{
		Title:  "Example 3: Events",
		Width:  800,
		Height: 600,
		EnabledEvents: []canvas.Event{
			canvas.MouseDownEvent{},
			canvas.MouseMoveEvent{},
			canvas.TouchStartEvent{},
			canvas.TouchMoveEvent{},
			canvas.KeyDownEvent{},
		},
	})
	if err != nil {
		log.Fatal(err)
	}
}

func run(ctx *canvas.Context) {
	d := &demo{}
	for !d.quit {
		select {
		case event := <-ctx.Events():
			d.handle(event)
		default:
			d.update()
			d.draw(ctx)
			ctx.Flush()
		}
	}
}

type demo struct {
	quit bool
	// ...
}

func (d *demo) handle(event canvas.Event) {
	switch e := event.(type) {
	case canvas.CloseEvent:
		d.quit = true
	case canvas.MouseDownEvent:
		// ...
	case canvas.MouseMoveEvent:
		// ...
	case canvas.TouchStartEvent:
		// ...
   	case canvas.TouchMoveEvent:
		// ...
   	case canvas.KeyDownEvent:
		// ...
	}
}

func (d *demo) update() {
	// ...
}

func (d *demo) draw(ctx *canvas.Context) {
	// ...
}

Note that the canvas.CloseEvent does not have to be explicitly enabled. It is always enabled by default.

Alternatives

2D game engines:

License

This project is free and open source software licensed under the BSD 3-Clause License.