皆さんこんにちは、フロントエンドのスイカ兄貴です。
キャンバスのスケーリングは、グラフィック デザイン ツールの重要な基本機能です。
これにより、グラフィックスが配置されている世界をカメラを持ったように歩き回ることができ、レンズを通して見たいグラフィックスだけを見ることができ、カメラをズームインしてグラフィックスの詳細を確認することができます。また、カメラをズームアウトすると、複数のグラフ間の関係の概要が得られます。
さて、それではズームキャンバス関数を実装する方法を見てみましょう。
この記事のアニメーション デモは、私が開発しているグラフィック デザイン ツールからのものです。
https://github.com/F-star/suika
オンライン体験:
https://blog.fstars.wang/app/suika/
シーン座標系とビュー座標系
シーン座標系は、グラフィックスが配置される 2 次元平面ワールドで使用される座標系です。単位はピクセル(px)です。座標系の原点はキャンバス (キャンバス要素) の左上隅で、X 軸は右、Y 軸は下です。
グラフィックスはこの平面上に描画され、理論的にはその範囲は無限に拡張できます。(実際には上限を設けますが、この値も非常に大きい値です。無限大では意味がありません。浮動小数点数は値の範囲です)
ただし、ディスプレイの幅と高さには制限があり、長方形の範囲内のコンテンツしか表示できません。
したがって、「カメラ」、つまりエリアの一部だけを観察するビュー座標系を導入する必要があります。
実際には、元の実グラフィックスの座標の線形計算を行うことになります。
1 つ目は、カメラが原点から観察したいオブジェクトまで移動するのと同じように、特定の領域をビューポート内に移動することです。しかし実際には、物体が存在する平面は一方向に移動します。
次に、ズームを実行します。これは、カメラがターゲット オブジェクトにズームインまたはズームアウトするのと同じように、レンズの下でオブジェクトが大きくなったり小さくなったりする効果です。
変換は、移動と拡大縮小の2 つのステップだけです。
行列変換を表示する
シーン座標系からビュー座標系への変換は、ビュー行列を乗算することによって実現されます。
実際、任意の 2 つの座標系における座標の変換は、行列の乗算によって実現できます。
行列と行列の乗算については、私のこの記事を読むことができます。
1 つ目は、座標、x 方向の変位-viewport.x
、および y 方向の変位をシフトすることです-viewport.y
。ここでは負の数ですが、移動しているのはキャンバスであるため、「カメラ」を移動したいのです。
<平移矩阵> * 坐标
次にズームします (ズーム値を表すためにズームを使用します)。
<缩放矩阵> * 平移后的坐标
一緒に書かれたすべてのプロセスは次のとおりです。
<缩放矩阵> * <平移矩阵> * 坐标
行列の乗算は結合的であるため、ビュー行列は次のようになります。
<视图矩阵>
= <缩放矩阵> * <平移矩阵>
行列は次のように表されます。
計算の結果は次のようになります。
対応するCanvas 2Dコード:
ctx.scale(zoom, zoom);
ctx.translate(-viewport.x, -viewport.y);
メソッドとして書かれたもの:
// 场景坐标转视图坐标
function sceneCoordsToViewport(x, y, zoom, scrollX, scrollY) {
return {
x: (x - scrollX) * zoom,
y: (y - scrollY) * zoom
};
}
逆の場合、シーン座標系はビュー座標に変換され、その逆行列を計算できます。
カーソルの周りをズームする
まず本質を認識しましょう、いわゆるカーソル周りのズームとは何でしょうか?
ビュー座標系内でカーソルが置かれている点の、ビューポートの左上隅からの相対位置は変更されません。
行う必要があるのは、ビュー座標系におけるカーソルとビューポートの左上隅の間の距離が変わらないように、ズーム変更後に viewport.x と viewport.y の値を調整することです。
ここで追加すべき知識のポイントです。これは、2 つの座標系での距離の変換です。
- シーンがビューに変わり、距離が に変換されます
dist * zoom
。 - ビューからシーンへの変換と距離は です
dist / zoom
。ビューポートに表示されるグラフィックスはズーム (ズーム倍) の結果であるため、その逆を減算する必要があります。
実現アイデアは次のとおりです。
- ズームする前にカーソル位置のシーン座標を記録します。
(cx, cy)
古いズームでシーンの座標を計算します。- 新しいズーム比 (ズーム) で cx を計算します
(cx / zoom, cy / zoom)
。 - 次に、この 2 つを減算して、新しい満足のいく左上隅の座標を取得します。
コードは次のように実装されます。
/**
* 以某点为中心,进行画布缩放
* @param {number} zoom 新的缩放比
* @param {number} cx 缩放中心(使用视图坐标)
* @param {number} cy
*/
const setZoomAndUpdateViewport = (zoom, cx, cy) => {
const prevZoom = this.zoom;
this.zoom = zoom;
// 计算缩放中点的场景坐标
const {
x: sceneCX, y: sceneCY } = viewportCoordsToScene(
cx,
cy,
prevZoom,
this.viewport.x,
this.viewport.y,
);
// 核心代码
const newViewportX = sceneCX - cx / zoom;
const newViewportY = sceneCY - cy / zoom;
this.viewport.x = newViewportX;
this.viewport.y = newViewportY;
this.renderScene();
};
キャンバスの周囲を拡大縮小する
ズーム値を手動で入力するなどしてズームするときにカーソルがキャンバス上にない場合は、キャンバスの中心でズームされます。
実装は上記と同じですが、 cx と cy が、受信ビューポート (つまり、キャンバス) の幅と高さを 2 で割った値に変更される点が異なります(viewport.width / 2), (viewport.height / 2)
。
終わり
キャンバスのスケーリングを実現するには、シーンの座標とビューの座標の関係を理解することが重要です。
シーン座標をビュー座標に変換するには、まずシーン座標の原点がビュー座標の原点と揃うようにキャンバスを移動し (シーン座標は -viewport.x および -viewport.x を移動します)、次にズーム (ズームで乗算します)。
私はフロントエンドのスイカ兄弟です。私をフォローして、グラフィック エディターについて詳しく学んでください。