Electron＋Canvas＋WebGL＋Reactのお絵描きツールの進捗

この記事は [Web グラフィックス Advent Calendar 2020](https://qiita.com/advent-calendar/2020/webgraphics) の11日目の記事です。

# はじめに

筆者は趣味でベクター系のお絵描きツールを作っています。この記事では、そのツールの開発の進捗というかやったことを紹介させていただきます。すごく雑多です。

[ライブデモ](https://warotarock.github.io/manual_tracing_tool/dist/)
画面左上のフォルダのアイコンのボタンからデモ用のファイルを開くことができます。

[ソースコード GitHub warotarock/manual_tracing_tool](https://github.com/warotarock/manual_tracing_tool)

![Image1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/59191/0fc22fbc-e229-3bef-e18b-01abe4e95438.jpeg)

プログラムの構成はおおまかに以下のようになっています。

- 実行環境： Electron
- UI： HTML (React)
- ドキュメントの描画、その他： Canvas2D
- ポージング３Ｄ（3Dデッサン人形）の描画： WebGL

![Image5.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/59191/a0b45771-ebef-0c0f-228c-2ac7f343e4a5.jpeg)

ドキュメントというのはユーザーの描く絵データのことです。ベクター形式のデータで、canvas要素による２Ｄ描画いわゆるCanvas2Dで描画しています。

また、人間の作図補助として３Ｄデッサン人形を表示する、「ポージング３Ｄ」と呼んでいる機能があります。これはWebGLで描画します。3D用のcanvas要素の上に2D用のcanvas要素をz-index指定で重ねて表示しています。

他にもその他の表示のためのcanvasがあります。ドキュメントとは再描画が必要になる場合が違うので、分けています。そのため合計三つのcanvas要素を重ねて表示していることになります。なんだか重そうですが、開発に使っている初代MS Surface Proでもそこそこ快適に動作しています。まだ初代使ってるのかって感じですが、今年こそ新型のSurfaceが出たら買おうと思ってたのに出る気配すらないんですよね…


# 進捗・今年やったこと

- UIの実装をCanvas2DからReactに変更
- モバイル対応を目指して画面構成を全体的に変更
- 左手用ＵＩ
- 曲線の描画のアルゴリズムの変更
- 目の左右対称の作画補助機能

# UIの実装をCanvas2DからReactに変更

本格的にＵＩを作りこむのにあたり、ほとんどのＵＩをReact (React hooks)で実装しなおしました。Canvas2D（特にライブラリも使わず）だと最初作るときはいいのですが、変更しようと思ったとき色々大変でした。

Reactでの実装は、仕様がすでに決まっているのもあって割と楽でした。ただ、実装が若干特殊になってしまったかもしれません。

システムのコア部分（以下コアシステム）はrequestAnimationFrameで常時ループしています。ただし描画処理は必要になったときだけ行います。マウスイベントなどのユーザーの入力イベントごとに再描画するのではなく、できるだけ決まったフレームレートでの再描画をするようにしたかったので、そのようにしています。そのほうが安定してペン等で入力できると思います。

そうなるとReactは言ってみればコアシステムとは独立して動作するので、コアシステムとReactを連携させる必要があります。それは次のようなインターフェースを定義してコアシステム側とRectコンポーネント側から関数をセットして相互にアクセスできるようにしています。

```ts

export interface UI_ColorMixerWindowRef {
  update?(color: Vec4): void; // UIを更新する関数
  color_Change?: (newColor: Vec4) => void; // コールバック関数
}
```

コアシステム側はいつどこでセットしてもいいのですが、Reactコンポーネント側はuseEffectを使ってコンポーネントが生成されたときにセットするようにします。

```tsx
  React.useEffect(() => {

    uiRef.update = (color: Vec4) => {
      UI更新処理
    };

    return function cleanup() {
      uiRef.update = null;
    };
  });
```

このやりかたが hooks の正しい使い方なのか自信はありませんが、useEffect などのドキュメントを見る限り大丈夫そうなので、いいのかなーと思いながらもこれで作っています。

# モバイル対応を目指して画面構成を全体的に変更

もう４年くらいの間、自分で使うだけのツールとして作ってきましたが、欲が出てきて誰でも使えるようにしたくなってきました。

というかアプリにしてお小遣いを稼ぎたい（爆）

今までは割り切ってほとんどキーボードで操作することでＧＵＩの実装の手間を省く作戦だったのですが、モバイル（スマホとタブレット）でも使用することを考えてＵＩを再設計することにしました。

一番問題だったのは、ツールウィンドウです。ブラシや消しゴムといったツールを選択するウィンドウです。

![Image11.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/59191/d94e2d9e-b8ee-6622-ca51-c339ba5b21c2.jpeg)


このウィンドウは色々な操作方法でスクロールができて、ホイール、マウス右ボタン、マウス中ボタン、スペースキー、タッチ操作でスクロール可能という超ニッチな仕様です。さらに、各項目に追加のボタンが表示できるようになっていて（画像右）、ポージング機能の入力の手前/前奥の指定ができるし、さらなる拡張も考えられる、というこだわりのＵＩでした。~~今見るとかなりかっこわるいですが。~~

それで何が問題かというと、配置する場所です。

![Image14.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/59191/cd1fe287-74dc-b3b7-438f-fba6597d1f47.jpeg)


作り始めた当初、このウィンドウは右にありましたが、他にもウィンドウが増えてきたときに左下に移動しました。その後、モバイル対応のために下に移動したり、右に戻したりもしてみましたが、最後に上に置いて横スクロールにしてみたところ、なんだが見覚えのある感じに……。

![Image16.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/59191/2a222301-bf1e-a46f-ec7a-711dbc5cf161.jpeg)

「こ、これは要するにリボンＵＩッ…！」
（スクショが無かったので上のは現在のツールの画像です）

ということで、素直にリボンＵＩっぽくしました。一部そのままですが。

![Image15.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/59191/88779813-64fc-89dc-774f-1b8e071d7aa4.jpeg)

うーん、ナウでヤングでミニマルなマテリアルになった気がする。

しかしそういえば、最近世間で熱の高まりを感じるBlenderは2.5でそれまで横型のリボン風ＵＩだったのをやめて縦型にしたんだったような気がします。たぶん、項目数が非常に多かったり増えたりするときは縦型がいいのではないかと思います。縦型だとスクロールするのに違和感がないです。逆にある程度少ないのであれば、横リボンのほうがライトなユーザーには見やすくていいのではないかと思います。

# 左手用ＵＩの実装

実は左下のＵＩだけはCanvas2Dで描画しています。

![Image31.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/59191/cf002e9a-d149-817f-5615-f87f62b76f51.jpeg)


理由は、二か所以上のタッチによる操作に技術的問題があって、そうせざるを得なかったからです。

背景から説明します。

デジタルで絵を描くときの一般的なスタイルとして、右手にペンを持って線を描いたり色を塗ったりしつつ、左手でキーボードやテンキ―、お絵描き用デバイス等を操作するというスタイルがあります。筆者もそうです。

しかし、スマホやタブレットは持っているけれどキーボードは無い、という人も一定数います。

そこで左手用ＵＩを画面左下に設けました。

![Image17_.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/59191/5f68c1a7-ce31-f188-1ca1-e1d1272c5700.jpeg)

スペースキー＋マウスドラッグによるビューのパン操作などと同じ操作ができるＵＩです。パン操作であれば、左手でキーボードを操作するかわりに、左手用ＵＩの十字矢印のアイコンをタッチしてから右手でキャンバスをドラッグすることで、パン操作ができます。

ところが、これをＨＴＭＬで実装したところ、タッチイベントが期待したように発生しないという問題が発生しました。二つのタッチが違うＤＯＭ要素をタッチしているからなのか、なぜか先にタッチしていたほうのイベントが発生しなくなったりするんですね。

この問題を回避すべくいろいろと頑張っても、ブラウザやReactの内部仕様が変わったらどうなるか不安だったので、左手用ＵＩだけ右手でタッチするのと同じcanvasにCanva2Dで描画することにしました。一つのcanas要素を触っているかぎりはイベントの問題は発生しないからです。

Canvas描画やWebGL描画のＵＩライブラリがありますが、そういったライブラリならこの問題は発生しないかもしれませんね。

# 曲線の描画のアルゴリズムの変更

隠し機能（－キー）で曲線の描画をWebGLで遅延描画するモードがあるのですが、今までは線の端部がポリゴンそのままの四角になっていました。

![Image30.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/59191/cbcc29f4-7db9-bdce-325a-c3ac4e6993a2.jpeg)


端部を丸くしたいと思い、Shadertoy などを参考に距離基準の実装方法に変えました。

変更前の計算方法では線の輪郭をベジエ曲線で補間していましたが、変更後は一本のベジエ曲線からの距離で輪郭が決まるようになりました。そのため端部に四角形にポリゴンを追加するだけで端部を丸くできます。

![Image21.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/59191/733ff5b0-90ba-0ba3-c0b5-336467340d00.jpeg)

端部とそれ以外でGLSLを変えないですむのが素晴らしいです。速度的にも有利なのではないかと思います。その反面、線の太さが線形補間されるようになったので、線の頂点数（＝曲線の分割数）が少ないと輪郭が直線的になるという問題もあります。


分割数が少なくても滑らかな線が描きたいがためにWebGLで実装しているので、ここはもう少し改善したいところです。

# 目の左右対称の作画補助機能

## ある日

目を作画してるワイ「あかん…目がなんかバランスおかしいのはわかるけど、どうおかしいのかわからん…」

![20201211_01.v_01.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/59191/fab274c4-5742-ae66-8124-61748377a8b7.jpeg)


ワイ「せや、左右反転してみろってよくいうやん。してみたらなんかわかるやろ。ぽちっとな」

![20201211_01.v_02.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/59191/a29215bd-0c94-3eec-9d4c-590b0a795ff3.jpeg)

ワイ「あかん…さっぱりわからへん…」

ＰＧじゃなければ存在していたかもしれない娘（0歳）「パパ、上下反転してみて？」

ワイ「せやな、左右がだめなら上下やな」

![20201211_01.v_03.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/59191/84d26eb6-c63e-3fa6-dca7-855fbc886bb1.jpeg)

ワイ「って、なおのことわかるかいっ。というか誰と話してるんやワイ…」

ワイ「どうすればええんや…なんかいいツールとかないんか…」

ＰＧじゃなければ存在していたかもしれない嫁**「あるで」**

ワイ「ファッ！？　だから誰！？」

## 作ってみた

（心で某太郎氏に土下座しながら）そこで、ポージング機能で目の部分に描いた線が左右反対側に反転して描画される補助機能をためしに作ってみました。

![Image28.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/59191/1be14f02-28a2-9add-2d4e-6aace981e84f.jpeg)

３Ｄデッサン人形の目の位置に眼球のような球面があるとして、その位置に描かれた線を球面に投影することで三次元座標を求め、左右反転して描画しています。上の画像でうっすら青い円が見えるのが球面を表しています。

投影に球面を使うのは、まぶたの縁は必ず眼球の表面上に位置するはずだからです。今回はデフォルメされた絵柄ですが、リアルな絵柄ではこの球面は眼球にほぼ一致することになると思います。

この機能はつい先日できたばかりなので、まだきちんと使ってみていませんが、気づいた点がいくつかあります。

- 自分で思う以上にきっちり左右対称になってしまい、逆に違和感を感じたりする。調整にはコツがいりそう。もしくはアタリにだけ使用するとよい
- 眉毛の部分は球面の奥まったところに投影され、奥まった位置で左右対称になるので、正しくならない。ヨッシーならいけるかも
- 瞳は視線がまっすぐ前を見ているか、寄り目なら左右対称にできる。視線が左や右を見るのは左右対称ではできない

仮題はありますが、個人的にはわりと良い結果が得られたと思います。今後は眉毛をどうにかしたいのと、デッサン人形の種類を増やしたり、カスタマイズできるようにできたらよいかなと思っています。

# おわりに

筆者はあまりアウトプットしないですが、アドベントカレンダーだけは良い機会として楽しみにして、この一年、記事に書いたようなことに取り組んできました。お絵描きツールもやりたいことがまだ山積みですし、また来年もこうして参加できたらいいなと思っています。

雑談みたいな記事になりましたが、ここまで読んでいただきありがとうございました。

それでは皆様よいお年を。