【Javascript】PWAでお絵かきアプリを作った時の技術的課題について【Apple Pencil】

## 成果物

どんなものを作ったか？を先に見てもらう方が分かりやすいと思うので成果物から先に置いておきます。

- [大人用](https://kurokky.github.io/whiteboard/tablet)(画像を保存できる)
- [こども](https://kurokky.github.io/whiteboard/child)(画像を保存できないシンプル版)

作る経緯についてはnoteの[こどものためにスマホ・タブレットで使える「ホワイトボード」を作った話](https://note.com/kurokky/n/ne4260f983498)に記載しています。  
qiitaは技術的な課題と解決方法について同不順で記載します。

## debugが超絶面倒だった

個人開発について回る問題ですけど。

- iPad(HTML/PWA)
- iPad + **Apple Pencil**(HTML/PWA)
- iPhone(HTML/PWA)
- Android(HTML/PWA/OS違い)
- FireTablet(HTML/PWA)

これを1人でチマチマやるのが、面倒でした。（家にある端末7端末ぐらい行き来したので…）

## AndroidのみPWAで画面サイズが取得できない。(小さくなる)

canvasの性質上、画面一杯の描画領域を持つ為にはHTML要素のwidthとheightで指定しなければなりません。(cssではおかしくなる)  
この為、通常のhtml版ではwindowのonload時に以下のようなロジックにしていました。

```js
const canvas = document.getElementById("canvas_name")
canvas.width = Math.max.apply( null, [document.body.clientWidth , document.body.scrollWidth, document.documentElement.scrollWidth, document.documentElement.clientWidth])
canvas.height = Math.max.apply( null, [document.body.clientHeight , document.body.scrollHeight, document.documentElement.scrollHeight, document.documentElement.clientHeight])
```

ブラウザ(chrome)では上手く行くのですが、PWAにして起動すると描画領域が200x300くらいになってしまい使い物になりませんでした。  
多分、splashが入る都合で取得が変になっているんだと勝手に解釈し & こちらの都合で検証時間がなかったため（割り切りも大事）、**Androidの場合500m秒経ってから再度取得しに行くロジック**に変更しました。

## PWAになるとhtml2canvasが動かない

画像保存機能として[html2canvas](https://html2canvas.hertzen.com/)を使っていたんですが、これがPWAになると正常に動作しませんでした。  
通信周りで色々問題があるんだろうなぁと(ロジック見てませんが、多分一旦JSで該当のURLのcssやら含むものをDLしてそれを見えないdomに展開してcanvasから画像化してると思います)

で、結局canvasを複数用意してそれをmergeというかjoin?するようなロジックにしました。

```js
//合成用のdom作成
const element = document.createElement("canvas")
element.width = Math.max.apply( null, [document.body.clientWidth , document.body.scrollWidth, document.documentElement.scrollWidth, document.documentElement.clientWidth]);
element.height = Math.max.apply( null, [document.body.clientHeight , document.body.scrollHeight, document.documentElement.scrollHeight, document.documentElement.clientHeight])
const ctx = element.getContext("2d")
const images = [new Image(), new Image()]
images[0] = document.getElementById("背景レイヤー用canvas").toDataURL()
images[1] = document.getElementById("メインレイヤー用canvas").toDataURL()
//元のメソッドにawait/asyncを付け回すのが面倒だったのでpromise使ってます
Promise.all(images.map(imageLoaded)).then(() => {
    ctx.drawImage(images[0], 0, 0, baseW, baseH)
    ctx.drawImage(images[1], 0, 0, baseW, baseH)
    let link = document.createElement("a")
    link.href = element.toDataURL("image/png")
    link.download = `${filename}.png`
    link.click()
})
```

レイヤー数が増えてくるとココはメモリと待機時間が心配になるのでちょっと今後レイヤー機能を持たせるなら課題になるところかと。

## マウスとタブレット、Apple pencilで座標位置が違う

マウスの場合mousemove、スマホ・タブレットの場合touchmoveに対してaddEventListenerを貼っているわけですが、このeventで取れる座標がそれぞれ違いました。  
具体的に言うと、横座標(x)が20ぐらい違いました。

```js
function draw(event){
    let x = event.offsetX
    let y = event.offsetY
    if(!isPC){
        event.preventDefault()
        const touch = event.touches[0]
        x =  event.touches[0].pageX
        y =  event.touches[0].pageY
        //指で触っている時は-20にする（Apple pencilとかはそのまま)
        if (touch.touchType !== 'stylus'){
            x -= 20
        }
    }
    //以下描画処理
}

```
## undo/redo機能

これは、どうしようか悩み最初はWebSQLを使う予定だったのですが、知らない間に廃止になっていて(SqlWebがあるらしいのですが)後方互換を考えると微妙な気がしたのでlocalStorageを使うことにしました。
具体的にはtoDataURLでcanvasのデータをURI形式にして、配列に追加して行く感じです。  

個人的にlocalStorageはネットカフェのような「1端末を複数人が使える環境だとデータを消し忘れがあったら嫌だなぁ」とかセキュリティに関して微妙な気持ちがあるんですが、今回の用途だとヒストリーとして持つのでそこは問題視しませんでしたが、肥大すると色々問題が出そうです…
とは言え、パッと回避方法が思いつかず一応閉じた時に消す処理は入れたので、及第点かなと思います。

```js
//閉じた時に強制削除
window.onbeforeunload = () => {
    localStorage.removeItem("ストレージ用のkey")
}
```

## 長押しでメニューが出ちゃう、変なところが選択状態になる

この辺はhtmlでタップゲームをやる際のTipsと同じですね。

iOSの場合はcssに

```css
body{
	-webkit-touch-callout:none;
}
```

Androidの場合は

```js
window.oncontextmenu = (event) => {
    event.preventDefault()
    event.stopPropagation()
    return false
}
```

で対応しましたが、そのおかげでPCで右クリックのメニューも出なくなりました…user-agentとかで厳密にやりたい人はちゃんとやりましょう…

## ダブルタップでzoomしちゃうのをやめさせたい

これも色々OSのversionなどによって対策がバラバラでしたが2022年9月の私の環境下では

```css
html{
	touch-action:manipulation;
}
```

で対応しました。

## cssでPC判定

レスポンシブならmax-widthなんですけど、そうじゃないのってどうやるんだっけ？と思ったら下記でいけました。  
（jsでUser-agent判定してるんでjsでやればいいんでしょうけどcssでできる部分は極力分離したかったのです。）

```css
@media(hover:hover) and (pointer:fine){
/*PCだけの場合の処理*/
}
```


## svgの最適化とか…
最終的にjs/css/html/svgを1つのファイルにしたんですが、svgの最適化をする為に[svgo](https://github.com/svg/svgo)を使いました。


## その他・感想

まぁ、降って沸いた子どもからの宿題だったので、速度重視でやりましたがやっぱりPWAにするってのはそれなりに慣れてないと面倒でした。(pwa出始めた頃に簡単なゲームを作った経験はあります)  
よくある85%くらいまでは、パッと作れるけど残りの15%が中々進捗上がらないってのは自分の知識や慣れがかなり左右するので、引き続きアップデートしていかなきゃなと感じました。

そして、ssl付きで広告なしのサーバーが無料で展開できるgithubpagesがあるのは楽だなぁ…と思いました。

まだ、息子からの全く違う課題が来たので、時間ある時に頑張ります。

P.S. 実家帰省中に息子から出た宿題だったのですがQiitaの[エンジニア夏休み企画！～自由研究や読書感想文を発表しよう～](https://qiita.com/official-events/6d31965c499a69377c0b)があったので個人開発のタグつけてみました。