JSを書かずにWebアプリを作る

## はじめに
前回、自分はrustでAPNGのライブラリを作りました。(良ければみてください [記事](https://qiita.com/poccariswet/items/677fda45f837f4d1dc3c))

作ったライブラリの活用手段として、Webアプリ化と自分の中で考え、今回Webアプリを作ろうと思いました。

## JSを書かない
JSを~~書きたくない~~ 書かない理由としては、rustはwasmを使ってWebアプリが書けることと、JSよりも高速かつメモリセーフといったようなメリットがあるためです。また、個人的に静的型付け言語が好きだからです。

## 作ったもの

もうすでに公開されているので、[ソースコード](https://github.com/poccariswet/drawasm)、[Webアプリ](https://drawasm.netlify.com/)が気になる方は先に見てみてください。

|https://drawasm.netlify.com|generated APNG|
|---|---|
|<img src="https://i.gyazo.com/2f3dd43bf7afcd0064649e61a4704c81.png" width="500">|<img src="https://i.gyazo.com/51f8f8c53753a515bbe5a00922b2fe6f.png" width="500">|

作ったアプリを簡単に説明すると、Web上で絵や文字を描きそれをAPNGに変換できるものです。

今回はその作ったもの説明を以下の順序で話していければと思います。

## 目次

- [下準備](#下準備)
- [wasmを実装したところ](#wasmで実装したところ)
  - [canvas部分について](#canvas部分について)
  - [state部分について](#state部分について)
        - [状態共有](#状態共有)
  - [toolbar部分について](#toolbar部分について)
        - [クリックイベントを取得する](#クリックイベントを取得する)
  - [canvasのpreview部分について](#canvasのpreview部分について)
  - [APNGをgenerateする部分について](#apngをgenerateする部分について)
- [今後](#今後)
- [終わりに](#終わりに)
- [参考](#reference)

<a id="下準備"></a>

## 下準備

まず、rustでwasmを書くためにツール群の[インストール](https://rustwasm.github.io/book/game-of-life/setup.html)、そしてテンプレートを使用して準備をしていきます。

wasmのツールをインストールし終わったら、
https://github.com/poccariswet/rust-wasm-template-webpack

```sh
$  cargo generate --git https://github.com/poccariswet/rust-wasm-template
```

これは自分が作った、templateで上記のコマンドをうって rustのwasmプロジェクトを作成していきます。

<a id="wasmで実装したところ"></a>

# wasmで実装したところ

<a id="canvas部分について"></a>
## canvas部分について

絵や文字書くための場所であるcanvas部分については、自分が以前書いた記事の [wasmのEventListenerの実装をコードとともに見てみる](https://qiita.com/poccariswet/items/aefa48835320aead4155)に飛んでみてください。
ここには、使っているライブラリ([web-sys](https://rustwasm.github.io/wasm-bindgen/api/web_sys/))などや**EventListener**について詳しくまとめているので読んでいただけたらと思います。

<a id="state部分について"></a>
## state部分について

今回の実装上、色やペンの太さ、キャンバスの画像の保持などを複数のデータや状態を共有しなければなりません。そのため、rustでは共有スマートポインタである、`Arc/Rc` を使用します。今回はスレッドでの共有がないと考えているため、**Rc** を使っていきます。

- Rc<T>は、同じデータに複数の所有者を持たせてくれる; Box<T>とRefCell<T>は単独の所有者。(*1)
- Box<T>は、不変または可変借用をコンパイル時に精査してくれる; Rc<T>は不変借用のみをコンパイル時に精査してくれる; RefCell<T>は、不変または可変借用を実行時に精査してくれる。(*1)

<a id="状態共有"></a>
### 状態共有
まず、状態共有をするには 内部可変性コンテナであるRefCellを使います。

- RefCell<T>は実行時に精査される可変借用を許可するので、RefCell<T>が不変でも、 RefCell<T>内の値を可変化できる(*1)

また、今回のアプリでは複数のオブジェクトから共有されている状態を持つので以下のように宣言します。

https://github.com/poccariswet/drawasm/blob/master/src/lib.rs#L60-L61

```rs
let state: Rc<RefCell<state::State>> = Rc::new(RefCell::new(state::State::new(canvas_w, canvas_h)));
```
このように宣言することで、***複数のオブジェクト間での状態の共有***ができます。

<a id="toolbar部分について"></a>
## toolbar部分について

アプリのこの部分です。
<img width="1223" alt="Screen Shot 2020-01-08 at 20.15.13.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192279/41ffd107-5d22-425a-a263-97af86199893.png">

ここでは、クリックイベントを取得して、それに応じてキャンバスに描くモードを変えたり、プレビューエリアへのプレビュー画像の追加をしていきます。

<a id="クリックイベントを取得する"></a>
### クリックイベントを取得する

https://github.com/poccariswet/drawasm/blob/master/src/toolbar.rs#L159-L187

```rs
fn create_color_picker(
    document: &Document,
    state: &Rc<RefCell<State>>,
) -> Result<Element, JsValue> {
    // divタグの生成
    let element = document.create_element("div")?;
    // toolbar用にスタイルの設定
    element.set_attribute(
        "style",
        "height: 50px; width: 50px; display: flex; align-items: center; justify-content: center; font-size: 11px; border: 1px solid #9b9b9b;",
    )?;

    // inputタグの生成
    let input = document
        .create_element("input")?
        .dyn_into::<HtmlInputElement>()?;
    // inputのattributeを設定
    // inputのタイプをcolorにしてvalueをblackに設定
    input.set_attribute("type", "color")?;
    input.set_attribute("value", "#000000")?;

    // stateをクローンして、Closure内で使用する
    let state = state.clone();
    let picked_color = Closure::wrap(Box::new(move |e: Event| {
        let target = e.target().unwrap().dyn_into::<HtmlInputElement>().unwrap();
        let color = target.value();
        state.borrow_mut().set_color(color)
    }) as Box<dyn FnMut(_)>);
    input.add_event_listener_with_callback("change", picked_color.as_ref().unchecked_ref())?;
    picked_color.forget(); //⚠️注意点
    element.append_child(&input)?;

    Ok(element)
}
```
上記の実際に、実装されたコード参考に見ていきます。

ここでやっていることは大きく分けて２つあります。
#### １つ目は、タグの生成、タグスタイルの設定
[Document](https://docs.rs/web-sys/0.3.9/web_sys/struct.Document.html)の `create_element`を使用して、elementの生成をします。生成されたelementに`set_attribute`を使用して、styleの適用をしていきます。今回はカラーパレットを使うので`type`を`color`にしています。<br>


#### 2つ目は、jsで言う`EventListener`のイベント制御の設定
“**inputのカラーが changeされた際に、`value`をstateのcolorに保存する。**“　
これがここのイベント制御の内容です。

JSの`EventListener`のClosureを実装するために、rustのクロージャをjsのクロージャに変換する、[Closure::wrap](https://rustwasm.github.io/wasm-bindgen/api/wasm_bindgen/closure/struct.Closure.html#method.wrap) メソッドを使用します。

その中で、`InputEvent`の`target.value`を取得したいので、[event.target](https://rustwasm.github.io/wasm-bindgen/api/web_sys/struct.Event.html#method.target)を`HtmlInputElement`に`dyn_into`をして、Eventの型をキャストします。キャスト後に[value()](https://rustwasm.github.io/wasm-bindgen/api/web_sys/struct.HtmlInputElement.html#method.value)でカラーパレットで選択されたカラーを取得します。

`Closure`を実装し終えた後に、`input`タグに[add_event_listener_with_callback](https://rustwasm.github.io/wasm-bindgen/api/web_sys/struct.Node.html#method.add_event_listener_with_callback)で`EventListener`の設定をします。

##### EventListenerをセットする際の注意点(forget)
以前の[記事](https://qiita.com/poccariswet/items/aefa48835320aead4155#3-forget)でも言っているのですが、作成したClosureを[forget](https://rustwasm.github.io/wasm-bindgen/api/wasm_bindgen/closure/struct.Closure.html#method.forget)することで意図的にメモリリークしています。(メモリリークと言っていますが、「このクロージャをリークして、プログラム全体の期間中有効にする」と言う意味です。)

こう言った理由で、Closureを**forget**しておかないと、スコープを抜けた際にdropしてしまうので`EventListener`として活用できなくなってしまいます。

以上のイベントを受け、canvas内では
https://github.com/poccariswet/drawasm/blob/master/src/draw.rs#L24-L55

```rs
    // mousedown
    {
        let context_copy = context.clone();
        let state_copy = state.clone();

        let mouse_down = Closure::wrap(Box::new(move |event: MouseEvent| {
        ...
         context_copy.set_stroke_style(&JsValue::from(state_copy.borrow().get_color()));
         context_copy.set_line_width(state_copy.borrow().get_pen_thin());
         context_copy.move_to(new_x, new_y);
        ...
```
のように、stateに保存されたcolorをmousedown時(絵を描くとき)に取得して、`set_stroke_style`メソッドを使用して描く色を変えています。
<br>
残りのtoolbar部分については、説明したinputの挙動と似ているので省略します(preview部分は次で説明)。


<a id="canvasのpreview部分について"></a>
## canvasのpreview部分について

さて次はcanvasに描いた絵をaddした際のpreview部分についてです。
<img width="300" alt="Screen Shot 2020-01-10 at 15.50.57.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192279/211459f0-8672-be0c-151c-b7f037611c76.png">

見てわかる通り、cssでbox-shadowでくぼみを作っているところがpreviewエリアになります。
ここに画像を追加するIconをクリックするとcanvasに書かれている画像が追加され、プレビューされます。

ここの実装は以下を見るとわかるようにまた、Closure内(クリックイベント)での実装となりまうす。
https://github.com/poccariswet/drawasm/blob/master/src/toolbar.rs#L265-L303

```rs
let handle_click = Closure::wrap(Box::new(move || {
        let img = document_copy
            .create_element("img")
            .unwrap()
            .dyn_into::<HtmlImageElement>()
            .unwrap();

        let url = canvas.to_data_url_with_type("image/png").unwrap();
        state.borrow_mut().add_preview_image(url.clone());

        // img set_src URL string
        img.set_src(&url);
        img.set_attribute("class", "preview-img").unwrap();
        img.set_width(state.borrow().get_preview_width());
        img.set_height(state.borrow().get_preview_height());
        preview.append_child(&img).unwrap();
    }) as Box<dyn FnMut()>);
```

このクロージャ内では、previewエリアに追加するためのimgタグの生成をし、そのimgタグの`src`に、canvasのメソッドである、[to_data_url_with_type("image/png")](https://rustwasm.github.io/wasm-bindgen/api/web_sys/struct.HtmlCanvasElement.html#method.to_data_url_with_type)を使用して`Base64`にしたimage情報を追加します。また、タイプが`png`になっているのは後々APNGをgenerateするためです。

その後は、前と同じように`set_attribute`や`set_width`、`set_height`で付加情報をつけ、previewのchildとして、追加します。

<a id="apngをgenerateする部分について"></a>
## APNGをgenerateする部分について

最後にプレビューされた画像をAPNGを作成する部分についてです。
https://github.com/poccariswet/drawasm/blob/master/src/generate.rs#L96-L166

ここでは、プレビューする際にstateに追加したbase64にエンコードされた画像データを`data:image/png;base64,`部分だけ取り除き、逆にデコードし`u8`のbufferにします。

rustの[image::load_from_memory_with_format](https://docs.rs/image/0.19.0/image/fn.load_from_memory_with_format.html)で`image::DynamicImage`(PNG)にエンコードします。その後に、**自分で自作した[APNGのライブラリ](https://github.com/poccariswet/apng)**にその値を入れAPNGにエンコードしていきます。

エンコード後、`Vec<u8>`のbufferが出来上がるので、それをblobに変換して、[window.open_with_url](https://docs.rs/web-sys/0.3.27/web_sys/struct.Window.html#method.open_with_url)で開けるようにしていきます。

https://github.com/poccariswet/drawasm/blob/master/src/generate.rs#L149-L159

```rs
        // ⚠️bufはAPNGにエンコードされたbuffer  
        let b = js_sys::Uint8Array::new(&unsafe { js_sys::Uint8Array::view(&buf) }.into()); 
        let array = js_sys::Array::new();
        array.push(&b.buffer());
        let blob = Blob::new_with_u8_array_sequence_and_options(
            &array,
            BlobPropertyBag::new().type_("image/png"),
        )
        .unwrap();
        let url = Url::create_object_url_with_blob(&blob).unwrap();
        let window = window().unwrap();
        window.open_with_url(&url).unwrap();
```

[Blob::new_with_u8_array_sequence_and_options](https://docs.rs/web-sys/0.3.24/web_sys/struct.Blob.html#method.new_with_u8_array_sequence_and_options)を使用し、blob作成していきます。
その前に、Blobを作成するために`buf`(APNGエンコード後のbuffer)をjs側(browser側)でも相互運用できるように`Uint8Array`型に変換し線形メモリへビューします。

https://rustwasm.github.io/wasm-bindgen/api/js_sys/struct.Uint8Array.html#method.view

> Views into WebAssembly memory are only valid so long as the backing buffer isn't resized in JS. Once this function is called any future calls to Box::new (or malloc of any form) may cause the returned value here to be invalidated. Use with caution!

のように、`js_sys::Uint8Array::view()`を使用する際には、`unsafe{}`ブロックを使用しなければなりません。

https://developer.mozilla.org/en-US/docs/Web/API/Blob/Blob
Syntax

```js
var newBlob = new Blob(array, options);
```
> array: 
An Array of ArrayBuffer, ArrayBufferView, Blob, USVString objects, or a mix of any of such objects, that will be put inside the Blob. USVString objects are encoded as UTF-8.

あとは、MDNに描いてあるように、blobを作成するための引数に描いてある通り、`js_sys::Array::new()`でArrayを作成し、中にキャストした`Uint8Array`を追加し、`blob`を作成していきます。

`blob`を作成後は、コード通り、urlを作成し、`open_with_url`で次のタブに出来上がった画像をプレビューします。

こんな感じです。
![preview](https://i.gyazo.com/bdbb02c102a1d521ad1cdd486e9c3287.gif)

<br>

あとは、この書いたコードをbuildし、WebAssemblyを吐き出してjs or html内で`run()`して終わりです！

<a id="今後"></a>

## 今後
一旦ここで完成ですが、色々と機能を増やしたいことがあるのでwasmで行けるところまで挑戦したいと思います。
以下にやっていこうと思うことを並べていきます。

- gifエンコードのサポート
- うごメモのようにclearしても背景に前に描いたやつが薄く見える状態にする
- 絵を描くときの透過性を変えれるボタンの追加
- previewされた画像を一枚一枚編集/削除できる ...etc


<a id="終わりに"></a>

## 終わりに

ここまで読んでいただきありがとうございます。
これを読んでいただいた方が、rustでwasmをやる興味を持っていただけたり、自分の記事がwasmをさわる際の参考にしてもらえれば幸いです。

また、JS書きたくないけど、Webアプリ作りたいと言う方が入ればぜひ試してみてください！


また、記事を読んでいて、疑問点やここ違くない？と言ってようなことがあればぜひ教えていただきたいのでコメント待ってます！


<a id="#reference"></a>
## reference
- https://qiita.com/qnighy/items/4bbbb20e71cf4ae527b9
- https://doc.rust-jp.rs/book/second-edition/ch15-05-interior-mutability.html [*1]
- https://wasmbyexample.dev/examples/webassembly-linear-memory/webassembly-linear-memory.rust.en-us.html
- https://docs.rs/js-sys/0.3.35/js_sys/
- https://docs.rs/web-sys/0.3.35/web_sys/