Jun 4, 2019

Recreate Dev.to offline using TinyGo & WASM

Dev's offline page is fun. Can we do that with (Tiny)Go and WebAssembly?

Dev's offline page is fun and we did implement it with Rust and WebAssembly.

Go is simple. Can we do it in Go and WebAssembly? And the answer is Yes...

I have the sample application here. If you want to write from scratch check this tutorial.

If you just want to see the source code, check out this branch.

Code Code Code

We will first import the "syscall/js" and "fmt" in the go/main.go.

The "syscall/js" package gives access to the WebAssembly host environment when using the js/WASM architecture. Its API is based on JavaScript semantics. This package is EXPERIMENTAL. :unicorn: :unicorn:

package main


import(
    "fmt"
    "syscall/js"
)

Add some variables globally available, these are the variables that we might need to use it when the functions are called from the JavaScript land or the browsers.

var (
	isPainting bool
	x          float64
	y          float64
	ctx        js.Value
    color      string
)

We will manually add the canvas element in the out/index.html.

<canvas id="canvas"> </canvas>

Remove the Hello World line inside the main function and replace it with the following.

Get the document object from the Browser. Using the document get the canvas element that we added just now.

We can do that by using the syscall/js API.

func main() {

    doc := js.Global().Get("document")
    canvasEl := doc.Call("getElementById", "canvas")
}

The API is simple. From the JavaScript's Global namespace get the document.

Inside the document call the canvas element by Id.

We will set some attributes of the canvasEl like width, height. We will also get the canvas' context.

bodyW := doc.Get("body").Get("clientWidth").Float()
bodyH := doc.Get("body").Get("clientHeight").Float()
canvasEl.Set("width", bodyW)
canvasEl.Set("height", bodyH)
ctx = canvasEl.Call("getContext", "2d")

We will then define the three event listeners mousedown | mousemove | mouseup to the canvasElement.

Mouse Down

We will start the painting when we clicked down the mouse.

startPaint := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
    e := args[0]
    isPainting = true

    x = e.Get("pageX").Float() - canvasEl.Get("offsetLeft").Float()
    y = e.Get("pageY").Float() - canvasEl.Get("offsetTop").Float()
    return nil
})


canvasEl.Call("addEventListener", "mousedown", startPaint)

Please note that return nil is important otherwise the program will not compile.

The entire API is based on Reflect API.

Mouse Move

We will define the paint function.

paint := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
	if isPainting {
		e := args[0]
		nx := e.Get("pageX").Float() - canvasEl.Get("offsetLeft").Float()
		ny := e.Get("pageY").Float() - canvasEl.Get("offsetTop").Float()

		ctx.Set("strokeStyle", color)
		ctx.Set("lineJoin", "round")
		ctx.Set("lineWidth", 5)

		ctx.Call("beginPath")
		ctx.Call("moveTo", nx, ny)
		ctx.Call("lineTo", x, y)
		ctx.Call("closePath")

		// actually draw the path*
		ctx.Call("stroke")

		// Set x and y to our new coordinates*
		x = nx
		y = ny
	}
	return nil
})
canvasEl.Call("addEventListener", "mousemove", paint)

Mouse Up

Finally, we will define the exit method that is called when the Mouse Up event is triggered.

exit := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
		isPainting = false
		return nil
	})

	canvasEl.Call("addEventListener", "mouseup", exit)

At last, we will define the color selector. Add a colors div in the out/index.html and the necessary CSS.

<style>
     .color {
            display: inline-block;
            width: 50px;
            height: 50px;
            border-radius: 50%;
            cursor: pointer;
            margin: 10px;
       }
</style>

<div id="colors></div>

We will define the colors as an array. Then loop through the colors and create color swatch node. Add an on click event listener to the color swatch.

divEl := doc.Call("getElementById", "colors")

	colors := [6]string {"#F4908E", "#F2F097", "#88B0DC", "#F7B5D1", "#53C4AF", "#FDE38C"}

	for _, c := range colors {
		node := doc.Call("createElement", "div")
		node.Call("setAttribute","class", "color")
		node.Call("setAttribute", "id", c)
		node.Call("setAttribute","style", fmt.Sprintf("background-color: %s", c))

		setColor := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
				e := args[0]
				color = e.Get("target").Get("id").String()
				return nil;
		})


		node.Call("addEventListener", "click", setColor)


		divEl.Call("appendChild", node)


	}

That is it.

Now start the web server by running go run webServer.go.

Compile the Go into WebAssembly module using

tinygo build -o ./out/main.wasm -target wasm -no-debug ./go/main.go

Visit the localhost:8080 to see the awesome canvas board ready for your creativity :unicorn:.

:bulb: The generated WebAssembly module is just 52KB. TinyGo is awesome and creates insanely smaller WebAssembly binaries. How does it create it? Stay tuned for the next post in this series. :bulb:

Note: Use Firefox / Firefox Nightly. Chrome sometimes crash.

More optimization :heart:

Remove the fmt import and replace fmt.Sprintf with a normal String concatenation, this will shave off another 26KB. Check the commit here.

Thanks to Justin Clift for sharing this tip :)

Do you :heart: Rust? check out here on how to use Rust for WebAssembly.

I hope this gives you a motivation to start your awesome WebAssembly journey. If you have any questions/suggestions/feel that I missed something feel free to add a comment.

You can follow me on Twitter.


Up Next


யாதும் ஊரே யாவரும் கேளிர்! தீதும் நன்றும் பிறர்தர வாரா!!

@sendilkumarn