はじめに
理系教科の指導にあたってしばしば問題の作図が必要とされます。私はよくsvgファイルを手打ちして作図するのですが、pathタグ内で放物線を描画するコマンドはありません。そこで、二次ベジェ曲線コマンドQ
を用いることで解決しようと思います(画期的だと思いましたが元をたどると自明な話でしたね(笑))。描画したい放物線の始点と終点と頂点の情報から、その放物線を二次ベジェ曲線で表したときの制御点を求め、その放物線の描画コマンドを出力するプログラムをpythonで作成します。
二次ベジェ曲線について軽い説明
二次ベジェ曲線は
座標平面上の3点(始点$P_0$, 制御点$P_1$, 終点$P_2$)を用いた
\vec{B}(t) = (1 - t)^2 \vec{P}_0 + 2(1 - t)t \vec{P}_1 + t^2 \vec{P}_2
の式で表されます( $t$ は $0 \leqq t \leqq 1$を満たす媒介変数)。
$N+1$個の制御点をもつベジェ曲線を$N$次曲線といいます。ベクターグラフィックなどでしばしば用いられます。
放物線(二次関数)について軽い説明
放物線(二次関数)は一般形$y=ax^2+bx+c$、標準形$y=a(x-p)^2+q$などで表される図形です。標準形は比例定数$a$によって放物線の凸の方向やその緩やかさが読み取れるほか、頂点が$(p, q)$で表せるので非常に便利です。
SVG(XML)でのQ
コマンドの仕様
Q
コマンドは二次ベジェ曲線を描画します。現在の座標から
Q <制御点のx座標> <制御点のy座標> <終点のx座標> <終点のy座標>
で描画できます。y軸は下方向が正となります。
本題
始点と終点と頂点を指定して制御点を求める
始点と終点および頂点から、制御点を求め、目標の放物線のコマンドを出力するプログラムを作ってみます。注意点として、指定した始点と終点を通り、指定した頂点を頂点に持つ放物線が存在することが条件です。
def InputPos(PosName):
RtnList = []
while len(RtnList) != 2:
RtnList = list(map(float, input(PosName + "の座標を空白区切りで指定:").split()))
if len(RtnList) != 2: print("正しく入力してください。")
return RtnList
P_0 = InputPos("始点")
P_2 = InputPos("終点")
Peak = InputPos("頂点")
a = (P_0[1] - P_2[1]) / ((P_0[0] - P_2[0]) * (P_0[0] + P_2[0] - 2 * Peak[0])) if P_0[0] == P_2[0] else 1
sign = (a * (P_0[0] - Peak[0]) * (P_2[0] - Peak[0])) / abs(a * (P_0[0] - Peak[0]) * (P_2[0] - Peak[0])) if P_0[0] != Peak[0] else 1
P_1 = [0.5 * (P_0[0] + P_2[0]), sign * ((P_0[1] - Peak[1]) * (P_2[1] - Peak[1])) ** 0.5 + Peak[1]]
print("\n-----目標のQコマンド-----")
print(f"M {P_0[0]} {P_0[1]} Q {P_1[0]} {P_1[1]} {P_2[0]} {P_2[1]}")
print("-------------------------")
こんな感じでいいのではないでしょうか。
始点$P_0(x_0, y_0)$および終点$P_2(x_2, y_2)$とすると、制御点$P_1$は
P_1\left( \frac{x_0 + x_2}{2},\ \pm\sqrt{(y_0 - q)(y_2 - q)} + q \right)
と表されます。ここでy座標の符号については、aの符号及び放物線の軸が始点と終点のx座標の間に入っているかどうかによって変化します。もう一度書きますが、注意点として、このプログラムが正しく機能する条件は、始点と終点および頂点が正しい場合です。
正しい例
$P_0(-5, 5)$, $P_2(0, 7.5)$, 頂点$(-3, 3)$の場合
この場合は該当する放物線が存在するため、正しい制御点$P_1(-2.5, 0)$を出力してくれます。
間違った例
$P_0(-5, 5)$, $P_2(0, 10)$, 頂点$(-3, 3)$の場合
この場合、該当する放物線は存在せず、間違った制御点を出力します。
この他にも正しい出力がされない例として、頂点が始点と終点のy座標の間にある場合があり、バリデーションを施していないことに注意してください。
私はこの条件で満足なのですが、もう少し使いやすいプログラムも書いてみたいと思います。
始点と終点と頂点のx座標のみ(=軸)を指定して制御点を求める
次は、始点と終点および頂点のx座標のみ(=軸)を指定することにします。
def InputPos(PosName):
RtnList = []
while len(RtnList) != 2:
RtnList = list(map(float, input(PosName + "の座標を空白区切りで指定:").split()))
if len(RtnList) != 2: print("正しく入力してください。")
return RtnList
P_0 = InputPos("始点")
P_2 = InputPos("終点")
p = float(input("頂点のx座標(=軸)を指定:"))
if P_0[0] == p and P_2[0] == p: print("指定した3点が同じ点です。")
else:
a: float
q: float
if P_0[1] == P_2[1]:
q = float(input("頂点のy座標を指定:"))
a = (P_0[1] - q) / ((P_0[0] - p) ** 2)
else:
a = (P_0[1] - P_2[1]) / ((P_0[0] - P_2[0]) * (P_0[0] + P_2[0] - 2 * p))
q = P_0[1] - a * ((P_0[0] - p) ** 2)
Peak = [p, q]
sign = (a * (P_0[0] - Peak[0]) * (P_2[0] - Peak[0])) / abs(a * (P_0[0] - Peak[0]) * (P_2[0] - Peak[0])) if P_0[0] != Peak[0] else 1
P_1 = [0.5 * (P_0[0] + P_2[0]), sign * ((P_0[1] - Peak[1]) * (P_2[1] - Peak[1])) ** 0.5 + Peak[1]]
print("\n-----目標のQコマンド-----")
print(f"M {P_0[0]} {P_0[1]} Q {P_1[0]} {P_1[1]} {P_2[0]} {P_2[1]}")
print("-------------------------")
こうすることで、頂点のy座標は勝手に見つけてくれますし、間違った出力もされません。
一応説明を付しておくと、$y=a(x-p)^2+q$式における$a$と$q$を求めています。
始点$P_0(x_0, y_0)$および終点$P_2(x_2, y_2)$として、
a =\frac{y_0 + y_2}{(x_0 - x_2)(x_0 + x_2 - 2p)}
q = y_0 - a(x_0 - p)^2
です。前式は2式 $y_0=a(x_0-p)^2+q$ および $y_2=a(x_2-p)^2+q$ を連立させ$a$について解くと導かれ、後式は$y_0=a(x_0-p)^2+q$を$q$について解くと導かれます。
さいごに
いい感じですね。二次関数が一般形で表されている場合は、さらに頂点を求めるプログラムもあればいいかと思います。
それでは、みなさんも良き放物線ライフを送りましょう🌈🌈