WASM? WebAssembly?
Lately I've started to ask myself: "Is WASM worth paying attention to?"
Let's find out. There are few languages that can be directly compiled into WASM. Anyway let's try GO.
We will make a simple web application that converts image from your webcam into ascii art.
The goal is to write as much code in Go as possible.
Let's go!
# go mod init asciifyme
And that's it, everything works!
Just kidding. It's not that simple.
We will need following pieces:
- 
webcam- will initialize and fetch image from web cam - 
canvas- we need this to fetch pixel data from image - 
asciifyier- turn image data into string 
The webcam:
This module will:
- create a 
videoelement withdocument.createElement - initialize webcam with 
navigator.getUserMedia 
We need to create a file webcam/webcam.go
First part looks like this:
package webcam
import (
    "fmt"
    "syscall/js"
)
var (
    navigator js.Value
    video     js.Value
)
func init() {
    navigator = js.Global().Get("navigator")
    video = js.Global().Get("document").Call("createElement", "video")
}
With this code we are creating video element, and fetching navigator for future use.
It's time to setup webcam:
func Setup() js.Value {
    user_media_params := map[string]interface{}{
        "video": true,
    }
    navigator.Call("getUserMedia", user_media_params, js.FuncOf(stream), js.FuncOf(err))
    return video
}
We will call this function from the main, it will setup webcam and return video object to fetch data from.
But wait! There are two callbacks stream and err we need to implement:
func err(this js.Value, args []js.Value) interface{} {
    fmt.Println("err")
    return nil
}
func stream(this js.Value, args []js.Value) interface{} {
    video.Set("srcObject", args[0])
    video.Call("addEventListener", "canplaythrough", js.FuncOf(canPlay))
    return nil
}
For now we will ignore errors and write error on console.
stream function adds a stream to the video element and listen to canplaythrough event.
Another callback? Yes! video will call canPlay callback when there will be enough data.
func canPlay(this js.Value, args []js.Value) interface{} {
    video.Call("play")
    return nil
}
When we have enough data press play!
We have a video, now we need a pixel data. Let's create canvas in canvas/canvas.go
package canvas
import (
    "syscall/js"
)
const (
    CanvasWidth  = 80
    CanvasHeight = 40
)
var (
    ctx js.Value
)
func init() {
    ctx = js.Global().Get("document").Call("createElement", "canvas").Call("getContext", "2d")
}
We're creating canvas element and fetching context. Will use it to draw and fetch pixel data.
func DrawImage(video js.Value) {
    ctx.Call("drawImage", video, 0, 0, CanvasWidth, CanvasHeight)
}
We can draw a frame from video by passing it into drawImage function.
func GetImageData() []uint8 {
    data := ctx.Call("getImageData", 0, 0, CanvasWidth, CanvasHeight).Get("data")
    lenght := data.Get("length").Int()
    goData := make([]uint8, lenght)
    js.CopyBytesToGo(goData, data)
    return goData
}
Fetching pixel data is more complicated. We have to fetch JS array of uint8 into GO.
This function takes the length of data from canvas, create GO array, and copy whole data into go array.
Voila! We have a pixel data.
What's left? Convert it to asciiart.
asciifyier/asciifyier.go is what we need!
package asciifyier
import (
    "asciifyme/canvas"
)
const (
    Chars       = "   .,:;i1tfLCG08@"
    CharsLength = 16
)
We don't need any JS stuff here. But need to import our canvas to fetch its size.
func Asciify(data []uint8) string {
    output := ""
    for y := 0; y < canvas.CanvasHeight; y++ {
        for x := 0; x < canvas.CanvasWidth; x++ {
            offset := (y*canvas.CanvasWidth + x) * 4
            red := data[offset]
            green := data[offset+1]
            blue := data[offset+2]
            //alpha := data[offset+3]
            brightness := (0.3*float64(red) + 0.59*float64(green) + 0.11*float64(blue)) / 255.0
            char_index := CharsLength - int(brightness*CharsLength)
            output += string(Chars[char_index])
        }
        output += "\n"
    }
    return output
}
What we're doing here? We're taking each pixel data from array of uint8 and creating a string. Our asciiart.
It's time for main.go ...
package main
import (
    "asciifyme/asciifyier"
    "asciifyme/canvas"
    "asciifyme/webcam"
    "syscall/js"
)
var (
    camera js.Value
    window js.Value
    pre    js.Value
)
func init() {
    camera = webcam.Setup()
    window = js.Global().Get("window")
    pre = js.Global().Get("document").Call("getElementById", "pre")
}
Taking all the pieces together. We will need a camera, window.requestAnimationFrame, and pre element to display our asciiart.
func loop(this js.Value, args []js.Value) interface{} {
    window.Call("requestAnimationFrame", js.FuncOf(loop))
    canvas.DrawImage(camera)
    imageData := canvas.GetImageData()
    output := asciifyier.Asciify(imageData)
    pre.Set("innerHTML", output)
    return nil
}
func main() {
    loop(js.ValueOf(nil), make([]js.Value, 0))
    select {}
}
In main loop we're:
- fetching data from 
video - drawing it on 
canvas - fetch pixel data from 
canvas - create asciiart using 
asciifyier - draw asciify into 
pre 
One more thing! select {} make the wasm program don't quit!
That's it. Compile time!
To run this in the browser we need:
- index.html
 - wasm_exec.js
 - compiled app
 
Simple index.html file
<html>
  <head>
    <title>asscify-me</title>
    <style>
      body{background-color:#000}pre{text-align:center}header{color:#daa520;font-size:18px;font-weight:700;text-shadow:0 0 3px gold}section{margin-top:30px;color:#32cd32;text-shadow:0 0 15px #0f0;font-size:14px}footer,footer a{margin-top:30px;color:red;text-shadow:0 0 15px tomato;font-size:14px}
    </style>
    <script src="wasm_exec.js"></script>
    <script>
    const go = new Go();
    WebAssembly.instantiateStreaming(fetch("asciifyme.wasm"), go.importObject).then((result) => {
    go.run(result.instance);
    });
    </script>
  </head>
  <body>
    <pre id="pre"></pre>
  </body>
</html>
And the build script:
#/bin/bash
export GOOS=js
export GOARCH=wasm
mkdir -p build
cp index.html build/
cp "$(go env GOROOT)/misc/wasm/wasm_exec.js" build/
go build -o build/asciifyme.wasm
Now we need to run:
# ./build.sh
Serve files from build folder, and use the browser.
Notice that the browser will give camera access when you're using https:// or localhost
P.S
But wait! The size! ~2MB is way to much! Yes!
Try thinygo compiler, ~200KB is much better!
# tinygo build -o build/asciifyme.wasm -target wasm
Don't need to write whole thing yourself if you don't want. Check out my github or working an app.
              
    
Top comments (0)