自動勉強会 vol.4 canvas の状態管理
11/5(金) 20:00 - 22:00
canvas の状態管理 subal.iconhashrock.icondaiiz.iconodiak.iconD.iconyuneco.iconbutaosuinu.iconoligin.icon
説明に図が必要なとき用ホワイトボード(画像クリックで飛びます)
https://kakeru.app/e0a1dd5d385e04c7cd3bb9b89045c355 https://i.kakeru.app/e0a1dd5d385e04c7cd3bb9b89045c355.svg
モチベーション
DOM ベース GUI は React でなんとでもなる
canvas の状態管理、特に canvas と DOM の状態管理が連携しあうときにみんなどうしてるのかD.icon
ゲームみたいに常時レンダリングループが回ってるものとお絵描きアプリみたいにDOMの一部としてイベントベースで動いてるものとはちょっと分けて考えたい気がする(どっちも知りたい)yuneco.icon
自分の場合脳味噌が古いのか、仮想domの差分評価が邪魔だなぁと思うことが多くcanvasとdomは負荷特性が違うと言う事を口実にして独自の最適化したくなります。ちなみにFlutterになると気分が変わります。 oligin.icon
何も考えずに作るとこんな感じ?hashrock.icon
Vue
canvasをコンポーネントにくるむ & refでcanvasの参照を取る
描画したいオブジェクトをツリーにしてdataに詰め込む(ローカルステート)か、propsから流し込む
draw methodを生やして状態を更新する度に全部再描画
気分に応じて親に状態の更新をemitする or vuexを使う
vuex使ってたけどvue 3移行が……
最近はProvide/injectも使ったり使わなかったりしてますbutaosuinu.icon
けど結局、オブジェクトとフォームとかの数値との同期とかめんどくさくてVuex頼りがち
Vue + pixi
Vueのdataとpixiのシーングラフを同期する必要がある…(しんどい)
後述のyunecoさんのやり方が良さそう
dataをwatchして更新があったらpixiの該当オブジェクトのプロパティを上書きする
例えば enemies: Enemy[] をデータに持つ場合、Enemyの生成・削除に応じてシーングラフにも追加・削除する必要がある
だのでVue側でシーングラフを持ちたいとなる
React
だいたいVueと同じですkeroxp.icon
あんまり canvas 案件やったことない前提ですが subal.icon
自分なら Redux に canvas の状態をもたせることを考えるかもと思った subal.icon
あるいは Rx ( subject$.subscribe(updateCanvas) )
react-redux の useStore が生きそうな数少ないシチュエーション
code:typescript
const store = useStore()
useEffect(() => {
store.subscribe(updateCanvas)
return () => store.unsubscribe()
}, [])
Reactのstateにcanvasのstateを持たせるのはパフォーマンス的に厳しいですkeroxp.icon
部分的にはありなのですが、mousemoveに付随した状態変更はReactでは捌ききれません(特にreduxだと…)
他UI部分との協調も必要なのでReactのstateも使います。つまりcanvas上ではstateが2つ混在します
canvasのみ対象の状態 -> イベントが起こるたび変更 -> 次フレームで反映
reactの状態 -> useEffectでcanvas側に反映 -> 次フレームで反映
この2つを毎フレーム反映していく感じになります。
基本的にはcanvasはrefを付けた以降はReactからは外れた場所で描画を行うことになります
subal.icon たとえば上の例でいうと、updateCanvas の中身が setState とかでなく DOM の操作を行う関数であればアリになる…?
大まかな分類
どこまで再描画するか
レンダリングループがある系
ゲームエンジンとか
requestAnimationFrameで全部再描画する
イベントベースで動く系
MouseEventやKeyboardEventで再描画?
保持する状態と管理手法
シーングラフ
他に何があるのだろう…全く分からない…hashrock.icon
差分検出・再描画範囲の管理
Fiber
タスクは5msで打ち切る
deadlineでコードを検索するとそれっぽいものが出てくる
インタラクションを優先する(キーボード入力等を阻害しないため)
ライブラリ・フレームワーク
pixi.js
Fabric.js
p5.js
react-pixijs
strapはreact-pixijsで作ってるそうですoligin.icon
react-paperjs
これどうだろう?(まだ触ってはいない)hashrock.icon
createjs
もう使うのやめたい
ゲームエンジン系のソリューション
ECS エンティティ・コンポーネント・システム
それUnityもそれですねD.icon
VueとPixiをいい感じに統合してくれるライブラリがないのが悲しい
自作ゲームでVueの状態管理をCanva(Pixi)側にも持ち込んでみたのはまあまあよかった(でもちょっと辛い)yuneco.icon
関連技術
シーングラフ
MSのcanvasパフォーマンスでKineticJSというライブラリが紹介されている(古いし公式サイトが乗っ取られてるので注意)hashrock.icon
canvasで作ったもの紹介コーナー
スクショとURL貼っていってください
実装の肝へのリンクが貼られてるとなおGood
ウィンドウシステムもどきhashrock.icon
https://gyazo.com/a9f7a8aa79884733484bfefdf204bdd9
まともVer
save() して translate() して drawChildren() して restore() する流れ、自分が書いたコードでも見たことあります~
細かいUIはDOMで、画面のメインエリアはCanvasです
縦書きできるのかっこいい
Webで作るってなったときに最初はHTMLでやろうかなって考えたんだけど、滑らかなズーミングとか縦書きとかお絵かきとかなんやかんやー>Canvasで書くしかないのかなと
Figma等WebベースのツールもCanvasで、いわゆる「キャンバス」なアプリはCanvasで作るということになりがち
こっち(Canvas)のほうがDOMベースよりパフォーマンスが出た
これって画面の全部をスクロール等ごとに一発で描画してますか?
何もしない場合は描き直さない、イベントごとに描画
フレーム外は描くオペレーションを発行しない。写っている部分は変更のたびに再描画
キャッシュはありますか?
メインのCanvasは一枚
OffScreenのCanvasがたくさんある
一枚のカードについて複数ある。ズームの最適化のためにmipmapを作っている
https://gyazo.com/12dc2dd384865356c94bbce3350d1f8f
LoD: 距離によって描画するvertの数を修正する
最初に開いたときに見た目を改善したいので通常のWebでもやっている
ツリー状にして、children&transform、当たり判定もtransformの逆行列
グルーピング楽ですね
グルーピング、かっこいいなあ
すごい
座標変換の行列計算つらい...
Canvasの中は別の世界
HTMLでやってるひとあります?
CacooはSVGだったはず
たしかGoogle DocsがごりごりHTMLでやってたのにCanvasに変えたって聞きましたね
レンダリングエンジンを統一したいっぽい
影 CanvasRenderingContext2D.shadowBlur 付けないかぎりはだいたい同じ挙動する
filter使えないの
影はつらい
svg-filterもSafatiのCanvasは使えなかったはず
つらすぎて影を別オブジェクトで実装した記憶
2dで描きつつエフェクトだけWebGL組み合わせるとかできる?無理?
firefoxだけパフォーマンス激悪
safariが早い説
WebGPU側でパフォーマンスが上がるか調査中
tokenをmetaタグで埋め込むような感じで面白い mactkg.icon
<meta http-equiv="origin-trial" content="TOKEN_GOES_HERE">
ボタンらしきものはだいたいReactで作っています
Reactの状態をCanvasとどういう風に持ち回しています?
stateが更新されたときに、Canvas側のstateにも反映して次のフレームで描画
ドラッグ中とか頻繁に状態が更新されるものはいったん消しておいて離したらReact側に反映
ReactはデフォルトでMouseMoveとかイベントを間引くので自前で
なんでもCanvasで描画するんではなくて、適度にDOMに任せる
当たり判定は矩形情報からがんばる
DOMのgetBoundingClientRect()
ctxのgetTransform()
ノードを持ちながら画面を回転してリサイズイベントを発生させたらどうなるかな...ということを考えてしまった
DOMとCanvasで座標連携するときはgetBoundingClientRect一発でいけるように、DOM側をシンプルにしておくのが必要かも。DOM側がスクロールとか持つと色々辛そう
reduxのstoreをCanvasと共有するとかいうことは
基本的にはreduxのstoreを使ってますけど、Canvas側の状態(サブストア)にも適宜入れ込み
それしか思いつかない
元のstoreをそのまま参照できれば single truth
Reduxはステート変わるごとに全コピー...でしたよね?持ってる状態が複雑だと死にそう
ドラッグするたびに、などreduxに反映したくない状態変更がある
localstate側に次々反映して最後にreduxに反映でもけっこうパフォーマンス出た
vuex -> 中身がvueのイベントシステムで実装されてるreduxみたいなやつ
ゲームのステート管理にreduxを使うのありですよね
PixiとかCanvasに対して、ツリーの一番上からトップダウンで変更伝えたいときはVuexとか大していらないんだけど、ゲームとかで、末端の方の要素が自律的に状態読み取って動き変えたい...みたいなときはVuexとwatchの組み合わせは便利だった
vuexに要素一覧入れておいて、変更をvueのイベントとして検知してrequestAnimationFrameで次タイミングで描画してる
全描画重い、面積について重いー>手描きのストロークとか素直に全パス組み立てると重いのでCanvasのclip使ってマウス周辺だけ描き直すように
再レンダリング範囲の計算面倒です
面倒だったけどパフォーマンスにはかなり効果が大きかった
カードのドラッグ中はカードの中身は変化しないのでカードの中身は画像としての描画一発でいける
PixiとかFlashととかだとchachAsBitmapみたいな
画像のほうが描画が軽い。図形のほうが重い
GPU使ってたら軽くなるのかな? mactkg.icon
画像の(変形とかない単純な)描画は純粋なメモリ転送なので速いはず
drawImageの引数でwidthとheightを指定する代わりにtransformかけると早い
図形描画はCPUでひとつひとつ処理しないといないから遅くなりがち?
ゲームエンジン系だと自前でUIまで出せる
けどアクセシビリティつらい。
webkit render node render tree を参考に描画命令が積まれる構造を作って、実際に描画される単位のツリーに再構築してレンダリングしている
Reactのレンダリングパフォーマンスの話
Reactのcommitフェーズとrenderingフェーズ、React17までは1-1でやっていた
スライダーの操作レスポンスがよくなる
18では描画が重い場合renderingフェーズをすっ飛ばせる
Canvasの描画はuseEffectでやることが多くて、renderの中ではやらない
mouseMoveの間隔に耐えられるようなUIの(ノートの上のメニューの)描画
固まる代わりにジャンプすることになる
資料
パフォーマンスやアクセシビリティへの言及あり
フォールバックコンテンツを<canvas>...</canvas>に入れてあげればOK
そもそもそのアプリの内容的にアクセシビリティいるのか問題(お絵かきアプリを目が見えない人が使うのか)
全員向けではなくても、使うか使えないか境界にいる人たちを意識して広げていく
Canvasのメモリリークについてなど