Canvas の panzoom はつらい
世界地図パズル では panzoom の実装を 200行くらいで実現していますが、作るのはものすごく時間が掛かった気がします。 ちなみに何を苦労したかというと、1つは Canvas ライブラリ fabric.js との擦り合わせです。 まずは Canvas だけで実装してみようと思ったのですが、その場合 setBackgroundImage() で地図を背景画像にし、zoom を実装することになります。 しかし拡大していくと再レンダリングされない問題が発生し、ジャギーが気になりました。 小さな SVG はきちんと再描画されているので、巨大な画像だけ別処理の扱いなのかな。 issue で同じ問題が提起されており、 現状この解決がかなり難しいとわかりました。 panzoom を実装するときは SVG などを使ったほうがずっと簡単です。とはいえ今回は fabric.js を使うのは絶対条件だったので、Canvas (ピース) と SVG (背景) で別々に panzoom を実装しました。 まずは fabric.js の zoomToPoint() や absolutePan() と timmywil/panzoom の両方を使って実装しました。 しかし座標変換の方法に差異があり擦り合わせに苦労し、また 7 の問題に直面し、セルフ実装を行うことにしました。 Canvas だと特定領域だけレンダリングをし直すようなビューが存在するので、計算が複雑になる問題があります。 これがなかなかつらい。
panzoom の実装のポイント
最後に汎用的に使えるコードをまとめておくと以下になります。 scale→translate する方式と translate→scale する方式がありますが、前者のほうが楽です。 scale を変えるときに複雑な計算がいらないのが大きいです。let panning = false;
let zoom = 1;
let px = 0;
let py = 0;
let dx = 0;
let dy = 0;
node.addEventListener("mousewheel", (event) => {
if (pannning) return;
zoom += event.deltaY * -0.01;
map.style.transform = `scale(${zoom}) translate(${dx}px,${dy}px)`;
});
node.addEventListener("mouseup", (event) => {
if (!panning) return;
panning = false;
const dx += (event.clientX - px) / zoom;
const dy += (event.clientY - py) / zoom;
});
node.addEventListener("mousedown", (event) => {
if (panning) return;
panning = true;
px = event.clientX;
py = event.clientY;
});
node.addEventListener("mousemove", (event) => {
if (!panning) return;
const x = event.clientX - px;
const y = event.clientY - py;
const tx = x / zoom + dx;
const ty = y / zoom + dy;
map.style.transform = `scale(${zoom}) translate(${tx}px,${ty}px)`;
});
マルチタッチがつらい
マルチタッチを考慮すると一気に実装が複雑になります。 タッチパネルを考慮して pinch zoom に対応する必要があったり、移動域を制限するなども手間です。 しかし一番厳しいのは、1点タッチが始まったとき、同時に 2点タッチが始まったとき、1点タッチの後に 2点タッチが始まったとき、 1点タッチが終わったとき、片方のタッチが終わったとき、同時に 2点タッチが終わったとき、 などかなりのパターンを考慮して実装しないといけない点です。 しかもイベントが非同期に発生するので、処理がぶつかることがあり、超大変でした。 3点タッチの実装なんて絶対に考えたくない…。ライブラリにしようかとも思ったのですが、fabric.js などと組み合わせるとほとんどの計算過程を保持しておく必要があって、綺麗にまとめるのは難しそうでした。 また panzoom なアプリを作ることがあれば考えてみたいですが、使うことないからなー。
0 件のコメント:
コメントを投稿