让我们去
#go #webassembly

wasm? WebAssembly?

最近我开始问自己:“ Wasm值得关注吗?”

让我们找出答案。很少有语言可以直接编译为WASM。无论如何,让我们尝试。

我们将制作一个简单的Web应用程序,将您的网络摄像头转换为ASCII ART。
目的是尽可能多地编写代码。

让我们走!

# go mod init asciifyme

就是这样!

只是开玩笑。这不是那么简单。

我们将需要以下部分:

  • webcam-将从Web Cam初始化和获取图像
  • canvas-我们需要它来从图像获取像素数据
  • asciifyier-将图像数据转换为字符串

网络摄像头:

这个模块将:

  • document.createElement创建一个video元素4
  • 用koude初始化网络摄像头5

我们需要创建一个文件webcam/webcam.go

第一部分看起来像这样:

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

使用此代码,我们正在创建video元素,并获取navigator以备将来使用。

是时候设置网络摄像头:

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
}

我们将从主调用此功能,它将设置网络摄像头并返回video对象以获取数据。
可是等等!我们需要实现两个回调streamerr

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
}

目前,我们将忽略错误并在控制台上写入错误。
stream功能在视频元素中添加了一个流并收听canplaythrough事件。
另一个回调?是的! video将在有足够的数据时致电canPlay回调。

func canPlay(this js.Value, args []js.Value) interface{} {
    video.Call("play")
    return nil
}

当我们有足够的数据播放时!

我们有一个video,现在我们需要一个像素数据。让我们在canvas/canvas.go中创建canvas

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

我们正在创建canvas元素并获取context。将使用它绘制和获取像素数据。

func DrawImage(video js.Value) {
    ctx.Call("drawImage", video, 0, 0, CanvasWidth, CanvasHeight)
}

我们可以通过将其传递到drawImage函数中从video绘制帧。

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
}

提取像素数据更为复杂。我们必须将uint8的JS阵列获取到GO。
此功能从canvas中获取数据长度,创建GO数组,然后将整个数据复制到Go数组中。
瞧!我们有一个像素数据。

剩下什么?将其转换为asciiart。

asciifyier/asciifyier.go是我们需要的!

package asciifyier

import (
    "asciifyme/canvas"
)

const (
    Chars       = "   .,:;i1tfLCG08@"
    CharsLength = 16
)

我们在这里不需要任何JS的东西。但是需要导入我们的canvas以获取其大小。

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
}

我们在这里做什么?我们正在从uint8数组中获取每个像素数据并创建一个字符串。我们的asciiart。

是时候到了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")
}

将所有碎片放在一起。我们将需要一个camerawindow.requestAnimationFramepre元素来显示我们的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 {}
}

在主循环中我们:

  • video获取数据3
  • canvas上绘制它
  • canvas获取像素数据
  • 使用asciifyier创建Asciiart2
  • 将assiify绘制为pre

另外一件事! select {}使WASM程序不要退出!

就是这样。编译时间!

要在我们需要的浏览器中运行此操作:

  • index.html
  • wasm_exec.js
  • 编译的应用程序

简单的index.html文件

<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>

和构建脚本:

#/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

现在我们需要运行:

# ./build.sh

build文件夹中使用文件,并使用浏览器。

请注意,当您使用https://localhost

时,浏览器将使访问相机访问

P.S

但是等等!尺寸! 〜2MB是很多!是!

尝试thinygo编译器,〜200KB好多了!

# tinygo build -o build/asciifyme.wasm -target wasm

如果您不想要,就不需要自己写整个东西。查看我的github或工作app