musicLineアプリ開発日記

作曲を誰でも楽しく簡単に♪

座標変換(2D)

今回はmusicLineの座標系と座標変換の話。
例えば、タップ位置の音符とは、どの音符を指すでしょうか?

タップ位置の音符とは



はじめに

タップ位置の音符は見たまんまの位置の音符では?と疑問に思われるかもしれません。
しかし、画面に表示している部分は楽譜全体の一部なので、表示している位置と拡大率の状態を考慮してタップ位置を計算する必要があります。
つまり、画面表示の座標系(スクリーン座標系)と全体の座標系(ワールド座標系)の関係が大切になります。

スクリーン座標系とワールド座標系
スクリーン座標系はワールド座標系の一部

この記事では、音符の描画とタップ位置を特定する座標変換について解説します。



音符の描画

ワールド → スクリーンの座標変換

作曲は音符の連なりを作ることであり、音符を特定の場所に置くことで発音タイミングと音階を指定します。楽譜はX軸が時間でY軸が音階を表現します。この絶対的な座標空間がワールド座標系です。

音符を置く

この場合、時間が88、音階が52の場所に音符があります。
ここで、楽譜全体が表示できるもの凄くでかい画面があれば問題ないですが、そんなことはなく、スマホの画面サイズにトリミングして表示します。
このスマホの画面に表示する座標系がスクリーン座標系です。

ワールド座標系の原点が左下にあるのに対して、スクリーン座標系は一般的に原点が左上にあります。

表示部分をトリミング

スマホ画面サイズ上に音符を表示するためには、左上原点のスクリーン座標に変換します。スクリーン座標の変換には、ワールド座標の他に座標変換ベクトルが必要です。

ワールド → スクリーンの座標変換

座標変換ベクトルは、スクリーン座標系からワールド座標系への移動量とスケールの変化量であり、画面を移動や拡大・縮小することで変化します。
なお、スクリーン座標系とワールド座標系ではY軸方向が異なるため、スケールYの変化量は常に-になります。

座標変換ベクトルの向き ワールドからスクリーンへ座標変換するので、「ワールド座標の原点をスクリーン座標の原点に合わせるのでは?」と勘違いしてました。座標変換は座標系を合わせたいわけではなく、「ワールド座標がスクリーン座標系でどのような座標になっているかを知りたい」という考え方です。
つまり、スクリーン原点からワールド原点を経由して頂点をつなぐベクトルを算出することが座標変換です。そのため、既知のワールド座標と座標変換ベクトル(スクリーン原点からワールド原点へのベクトル)を足すことでスクリーン座標がわかります。



ローカル → ワールドの座標変換

ワールド → スクリーンの座標変換して画面に表示することを解説しましたが、実際には音符はワールド座標で管理している訳ではありません。
musicLineではフレーズ上に音符を置く仕様になっており、音符の位置はフレーズ単位で管理しています。フレーズにローカル座標系を設定して、フレーズの原点からの音符位置を保持してます。この仕様により、フレーズ単位で音符の移動やコピーができるようになります。

そのため、音符を画面に表示するためには以下の流れで座標系を変換します。

  • ローカル座標系
  • ワールド座標系
  • スクリーン座標系


ローカルからワールドへ変換するための変換ベクトルはX軸のみ変化します。また拍子によって1小節に含まれる拍が異なるため、ベクトルの長さは小節番号で保持します。

ローカル → ワールドの座標変換



補足

四角形の座標変換

四角形における座標変換時の注意点です。
四角形を表現するとき、原点とサイズがありますが、変換するときに原点の位置が異なることがあります。(SwiftUIではCGRectで左上が原点)

四角形の原点が異なる


原点が異なる場合、座標変換後に原点位置を調整します。
この場合は、左下原点から左上原点へ調整します。

 
変換後に原点を移動

ワールド座標とスクリーン座標の場合、Yスケールが-なので変換後に高さが-になります。そのため、サイズが-にならないように原点を移動させると考えると調整しやすいです。


スクロール位置を保持しながら拡大

画面を拡大する時にスクロール位置がそのままだと、画面の中心がズレる問題があります。(正確には、画面の中心に表示されていたワールド座標が中心からズレる。)
そのため、拡大前と後でズレた量を戻す処理が必要です。

スクロール位置を維持しながら拡大

ちなみにズレ量を補正するベクトルはこんな感じで表すことができます。

ズレ補正ベクトル:画面中心 - 拡大前のズレた画面中心

screenHeigh / 2 - convertToScreen(baseWorldY)
  • screenHeight:画面高さ
  • baseWorldY:拡大前の画面中心のワールド座標Y
  • convertToScreen():ワールド→スクリーンの座標変換する関数


ボツ案を表示

ズレとかは気にせずに、とりあえずスクリーン位置を初期位置に戻して、初期位置から考える方法。
初期位置から拡大前の画面中心へ移動することでスクロール位置を維持します。

スクロール位置を維持しながら拡大(別解答)

ズレた位置を補正する方法の方が図解で理解しやすいと思ったのでそちらを採用しました。

ちなみにスクロール位置はこんな感じで表すことができます。

offset = 0
offset = -convertToScreen(baseWorldY) + screenHeigh / 2
  • offset:スクロール位置
  • screenHeight:画面高さ
  • baseWorldY:拡大前の画面中心のワールド座標Y
  • convertToScreen():ワールド→スクリーンの座標変換する関数




タップ位置の音符

タップ位置はスクリーン座標が既知となります。
そのため、タップしている音符を特定するためには同じ座標系(ワールド座標系)への変換が必要です。

タップ位置

タップ位置を座標変換により、ワールド座標を取得します。スクリーン→ワールドの座標変換はワールド→スクリーンで使用した座標変換ベクトルの逆ベクトルを使う事で変更できます。

スクリーン→ワールドの座標変換



おわりに

座標変換はわりと複雑なのであまり整理する気が進まなかったのですが、毎回毎回座標変換について考えることも大変なので、この機会にmusicLineの座標軸と座標変換についてまとめてみました。

あまり理解してなくても「デバッグしたら、上手く動作した」ということで放置してしまいがちなので危険ですね。
よくわからないロジックをよくわからないロジックで修正すると雪だるま式に不具合ができてしまうので、ブラックボックスが狭いうちに色々と頭の整理をしていきたいものです。

ちなみに今回初めてFigmaを使って図を作成してみました。ずっとPowerPointを使っていたのですが、なかなか使いやすいですね〜 ^ ^ y