DEV Community

Cover image for Create Dev's offline page with (Tiny)GO and WebAssembly ๐Ÿฆ„๐Ÿ’กโœจ
Sendil Kumar
Sendil Kumar

Posted on • Updated on • Originally published at sendilkumarn.com

Create Dev's offline page 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. ๐Ÿฆ„ ๐Ÿฆ„

package main 


import( 
    "fmt"
    "syscall/js"
)
Enter fullscreen mode Exit fullscreen mode

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
)
Enter fullscreen mode Exit fullscreen mode

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

<canvas id="canvas"> </canvas>
Enter fullscreen mode Exit fullscreen mode

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")
}

Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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)

Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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)


    }

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Visit the localhost:8080 to see the awesome canvas board ready for your creativity ๐Ÿฆ„.

๐Ÿ’ก 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. ๐Ÿ’ก

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

More optimization โค๏ธ

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 โค๏ธ 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.

If you like this article, please leave a like or a comment. โค๏ธ

Thanks

aspittel image

for the article.

Discussion (4)

Collapse
sayanarijit profile image
Arijit Basu

Any plans to do in python? If not I can give it a try.

Collapse
sendilkumarn profile image
Sendil Kumar Author

Go for it ๐Ÿš€๐Ÿ

Collapse
sendilkumarn profile image
Sendil Kumar Author

So cool :)