💥 発生した事象
SVGをドラッグして表示位置を移動する処理を実装していたとき、
一部のSVGだけY方向の動きが逆になることがありました。
左右方向は問題ありません。
ただ、上下にドラッグすると、
マウスを下へ動かす
↓
SVGは上へ動く
という状態になります。
同じ処理を使っている別のSVGでは正常に動いていたため、
最初は mousemove やドラッグ量の計算を疑いました。
調べていくと、
問題が起きていたSVGには、
transform="scale(1, -1)"
が指定されていました。
🚀 急いでいる人向け
SVGに、
transform="scale(1, -1)"
が指定されていると、
Y方向の座標系が反転します。
今回の処理では、2点の画面上の位置からY方向の倍率を計算していました。
sf.y =
100 / (g2Box.y - g1Box.y);
通常は、
g2Box.y - g1Box.y
→ 正の値
でしたが、Y反転されたSVGでは、
g2Box.y - g1Box.y
→ 負の値
になりました。
その結果、
sf.y
→ マイナス
となり、
viewBoxY =
viewBoxY - dy * sf.y;
でY方向がもう一度反転していました。
表示方向だけを正規化したい場合は、
Y方向の倍率を正の値として扱う方法があります。
sf.y =
100 / Math.abs(
g2Box.y - g1Box.y
);
ポイントは、
見た目が上下反転している
だけでなく、
座標差分の符号まで反転する
ことです。
💡 この記事で分かること
-
scale(1,-1)がSVGのY座標に与える影響 - 見た目は正常でもドラッグ方向だけ逆になる理由
-
getBoundingClientRect()のY差分が負になるケース - 座標倍率にマイナス値が入ると何が起こるか
- 反転されたSVGを扱うときの座標補正の考え方
はじめに
今回扱っていたSVGには、
生成元の違う2種類のデータがありました。
Visio由来のSVGではドラッグ操作が正常に動いていました。
一方、AutoCAD変換版のSVGでは、
上下方向だけドラッグが逆になります。
ドラッグ処理自体は共通です。
そのため最初は、
mousemoveの座標取得がおかしい?
dyの計算がおかしい?
viewBoxの更新方向が逆?
あたりを疑っていました。
ただ、イベント側の clientY などを確認しても、
マウスの移動量自体は正常でした。
違っていたのはSVG側の座標系でした。
🔍 原因
問題が起きていたSVGの大枠に、
<g transform="scale(1, -1)">
が指定されていました。
これは、
X方向:そのまま
Y方向:反転
という変換です。
つまり、
scale(1, 1)
→ 通常
scale(1, -1)
→ 上下反転
です。
今回のSVGでは、
個々の図形ではなく大きなグループ側に指定されていました。
そのためSVG全体がY方向へ反転していました。
ドラッグ処理はどうなっていたか
ドラッグ時には、
マウスの移動量 dx / dy を使って viewBox の位置を変更していました。
処理を簡略化するとこんな形です。
function updateViewBoxMin(dx, dy) {
const viewBoxList =
svg
.getAttribute("viewBox")
.split(" ");
viewBoxList[0] =
Number(viewBoxList[0])
- dx * sf.x;
viewBoxList[1] =
Number(viewBoxList[1])
- dy * sf.y;
svg.setAttribute(
"viewBox",
viewBoxList.join(" ")
);
}
X方向は、
dx * sf.x
Y方向は、
dy * sf.y
を使っています。
問題になったのは sf.y でした。
Y方向の倍率を計算していた
SVG上の基準点2つを使い、
画面上の距離から倍率を計算していました。
イメージとしては、
const g1Box =
point1.getBoundingClientRect();
const g2Box =
point2.getBoundingClientRect();
const baseLength = 100;
sf.x =
baseLength
/ (g2Box.x - g1Box.x);
sf.y =
baseLength
/ (g2Box.y - g1Box.y);
通常のSVGでは、
Y座標の差分が正の値になっていました。
例えば正常なケースでは、
g2Box.y - g1Box.y
→ 正
sf.y
→ 0.718
でした。
ところが問題のSVGでは、
g1Box.y = 1390.304...
g2Box.y = 918.231...
となっていました。
差分は、
918.231...
-
1390.304...
=
-472.073...
です。
その結果、
sf.y
→ -0.212
になりました。
ここで初めて、
ドラッグ処理ではなく、倍率の符号が逆になっている
ことに気付きました。
なぜ sf.y が負になったのか
原因は、
scale(1, -1)
です。
Y軸が反転しているため、
画面上で基準点1と基準点2の上下関係も反転していました。
通常は、
point1
↓
point2
だったものが、
point2
↓
point1
のような関係になります。
その状態で、
g2Box.y - g1Box.y
を計算すると、
負の値になります。
📖 なぜドラッグ方向まで逆になったのか
ドラッグ処理では、
viewBoxY =
viewBoxY
- dy * sf.y;
としていました。
通常、
dy > 0
sf.y > 0
なら、
dy * sf.y
→ 正
です。
ところが今回、
sf.y < 0
になっています。
すると、
dy * sf.y
→ 負
になります。
さらに、
viewBoxY - 負の値
なので、
viewBoxY + 正の値
になります。
つまり、意図していた方向と逆です。
整理すると、
scale(1,-1)
↓
基準点のY方向が反転
↓
g2Box.y - g1Box.y が負になる
↓
sf.y が負になる
↓
dy × sf.y の符号も反転
↓
viewBoxの移動方向が逆になる
という流れでした。
✅ 補正方法
今回のように、
倍率としては距離だけ欲しい
Y方向の向きはドラッグ処理側で決める
のであれば、
基準点間の距離を正の値として扱えます。
例えば、
sf.y =
baseLength
/ Math.abs(
g2Box.y - g1Box.y
);
です。
修正前は、
sf.y =
baseLength
/ (g2Box.y - g1Box.y);
なので、
-472.073...
がそのまま使われます。
Math.abs() を使えば、
472.073...
として扱えます。
そのため sf.y も、
正の倍率
として計算できます。
ただし、とりあえず全部 Math.abs() にすればいいわけではない
ここは注意したいところです。
今回のケースでは、
基準点間の距離
として倍率を使っていたため、
符号を持たせる必要がありませんでした。
そのため絶対値へ正規化する考え方が使えます。
一方、
座標系がどちら向きなのか
自体を判定したい処理では、
マイナスの符号に意味があります。
例えば、
const invertedY =
g2Box.y < g1Box.y;
のように、
反転状態を別に持つ方法もあります。
const yDiff =
g2Box.y - g1Box.y;
const invertedY =
yDiff < 0;
sf.y =
baseLength
/ Math.abs(yDiff);
これなら、
倍率
→ 常に正
座標方向
→ invertedYで管理
と役割を分けられます。
実装が大きくなる場合は、
こちらの方が分かりやすいと思います。
scale(1,-1) 自体を消せばいい?
今回のSVGでは、
scale(1,-1) はSVG生成時の座標系を合わせるために入っているものでした。
そのため、
ドラッグが逆になる
↓
scale(1,-1)を削除する
では済みません。
削除すると、
今度は図面自体の表示が上下反転してしまいます。
必要なのは、
表示用のtransform
と、
操作用の座標計算
を分けて考えることでした。
getBoundingClientRect() にもtransformは反映される
今回もう1つ重要だったのがここです。
element.getBoundingClientRect();
で取得する値は、
ブラウザ上に表示されている位置を基準にしています。
つまりSVG側で、
transform="scale(1, -1)"
が適用されていれば、
その変換後の位置関係が取得結果にも現れます。
元SVG内の、
x
y
だけを見ていると、
point2の方が下にあるはず
と思っていても、
ブラウザ上では上下が逆になっている場合があります。
SVGで座標がおかしいときは、
元の属性値
transform
getBoundingClientRect()
を分けて確認した方が追いやすいです。
💼 実務での確認順
今回のように、
一部のSVGだけドラッグ方向がおかしい場合は、
自分なら次の順番で確認します。
1. マウス座標
clientX
clientY
dx
dy
が想定した値になっているか。
ここが正常なら、
イベント取得以外を疑います。
2. viewBox更新式
viewBoxY - dy * sf.y
など、
符号を確認します。
3. 倍率
sf.x
sf.y
をログへ出します。
今回の場合、
sf.x → 正常
sf.y → マイナス
だったため、かなり絞り込めました。
4. 基準点の位置
getBoundingClientRect();
の結果を比較します。
g1Box.y
g2Box.y
の大小関係を見るだけでも、
Y反転に気付きやすいです。
5. 親要素のtransform
最後に対象要素だけでなく、
親 <g> まで確認します。
今回も問題の指定は、
個別の図形ではなく大枠側の、
transform="scale(1, -1)"
でした。
⚠️ 見た目が正しいから座標も正しいとは限らない
今回一番ハマったのはここでした。
SVG自体は問題なく表示されています。
そのため最初は、
SVGデータは正常
↓
JavaScriptのドラッグ処理がおかしい
と思っていました。
実際には、
SVGはtransform込みで正しく表示されている
↓
でも座標系は反転している
↓
その座標を使った操作処理でズレる
という状態でした。
表示確認だけでは気付きにくい問題です。
📝 まとめ
今回の原因は、
SVG全体へ指定されていた、
transform="scale(1, -1)"
でした。
ポイントは次のとおりです。
-
scale(1,-1)はY方向を反転する - 見た目だけでなく座標の上下関係も反転する
-
getBoundingClientRect()のY差分が負になることがある - Y差分から倍率を計算すると、その倍率まで負になる
- 負の倍率をドラッグ量へ掛けると移動方向が逆になる
- 距離として使う倍率なら
Math.abs()で正規化する考え方がある - 座標方向そのものが必要なら、反転状態は別で管理する
- SVGの表示用transformと操作用の座標計算は分けて考える
最初は mousemove 側を追っていましたが、
原因はSVGのかなり外側に付いていた transform でした。
一部のSVGだけ上下方向がおかしい場合は、
ドラッグ処理だけでなく親要素の scale も確認すると原因を追いやすいです。