import numpy as np import sympy as sp import matplotlib.pyplot as plt import matplotlib.patches as pat import matplotlib.animation as anima import math class Coordinate(): def __init__(self, init_x, init_y, m, n,line_side,dx,dy): # ボールの初期位置を設定している。 self.ball_x = init_x self.ball_y = init_y # line_y とは、ボールが領域内にあるかどうかの判断として使う変数である。 ## line_y[i] > 0 ならば ボールは直線y=m[i]x+n[i] の下側にあり、 ## line_y[i] < 0 ならば ボールは直線y=m[i]x+n[i] の上側にある。 self.line_y = [] # dx dy はボールが1回の描画で動く増加量である。 self.dx = dx self.dy = dy # dx==0 ならば、ボールはx軸に対して垂直に動いている。 ## 傾きが無限大のときは、infinity1=-99999という整数値とした。 ## vertical_dy はボールが垂直で動くとき、1回で動く量とした。 ## infinity1 と vertical_dy は main の中でGlobal 変数として定義している。 if self.dx != 0: self.old_incli = self.dy/self.dx else: self.old_incli = infinity1 self.dx=0 self.dy = vertical_dy def state_update(self): # ボールの次の位置を計算している。 ## ball_x、ball_y がボールの x座標と y座標を表している。 ## ボールが進む直線の傾きをnew_incli としている。 if self.dx != 0: new_incli = self.dy/self.dx else: new_incli = infinity1 self.dx=0 self.dy = vertical_dy # divider はボールの動く量を調整する変数であり、 ## 直線の傾きが大きいほど、dx の増加量に対してdy の増加量が極端に大きく ## なるので、その調整としてdividerを考えた。 divider = 1 if new_incli != infinity1: if abs(new_incli) > 8: divider = int(abs(new_incli)) elif abs(new_incli) > 4: divider = 2 # dd[i] は直線y=m[i]x+n[i] とボールとの距離を表す変数である。 dd=[] # distance はボールと直線の距離を求める関数である。 for k in range(3): dd.append(distance(m[k],n[k],self.ball_x,self.ball_y)) # 直線に近づくほど、増加量を小さくし、ボールの速度を遅くする。 if min(dd) <= 0.5: divider +=5 elif min(dd) <= 0.4: divider +=6 elif min(dd) <= 0.3: divider +=7 elif min(dd) <= 0.2: divider +=10 elif min(dd) <= 0.1: divider +=12 elif min(dd) <= 0.05: divider +=14 elif min(dd) <= 0.01: divider +=18 # 次に進むボールのx座標及びy座標を計算する。 self.ball_x += self.dx/divider self.ball_y += self.dy/divider # line_y[k] はボールと y=m[k]x+n[k] との y座標の差の値である。 ## ボールが直線の下側のとき+、上側のとき-になる。 self.line_y.clear() for k in range(3): self.line_y.append(m[k]*self.ball_x+n[k]-self.ball_y) # ボールが領域内にあるかどうかの判断をしている。 ## line_side[j] には、領域の指定が main で定義されているが、 ## 領域が境界線の上側は1、下側は-1 と定義されているので、 ## line_side[j] との積をとって、それが正になったら領域から外れている。 for j in range(3): if self.line_y[j] * line_side[j] > 0: # ボールが領域から外れたので、元に戻す。 self.ball_x -= self.dx/divider self.ball_y -= self.dy/divider # old_incli が跳ね返る前の進入するボールの直線の傾きである。 self.old_incli = new_incli # 入射角と反射角が等しいことから、跳ね返る直線の傾きを求めている。 new_incli = incli_line(m[j],self.old_incli) # 跳ね返った後のボールの増加量dx、dy を求めている。 self.dx,self.dy=define_dx_dy(self.dx,self.dy,m[j],new_incli,self.old_incli) break return self.ball_x, self.ball_y class Drawing(): def __init__(self, ax, marks): self.ax = ax self.marks = marks self.ball_img, = ax.plot([], [], color="red") def draw_circle(self, center_x, center_y, circle_size=0.02): # ボールを描くメソッド ## marks == True のとき、matplotlib のCircleメソッドを使って描画し、 ## marks == False のとき、媒介変数θを使った円の方程式 x=rcosθ、y=rsinθを使って描画する。 self.circle_x = [] self.circle_y = [] if self.marks: c=pat.Circle((center_x,center_y),radius=circle_size,fc='blue',ec='red') ax.add_patch(c) else: steps = 20 for j in range(steps): self.circle_x.append(center_x+circle_size*math.cos(j*2*math.pi/steps)) self.circle_y.append(center_y+circle_size*math.sin(j*2*math.pi/steps)) self.ball_img.set_data(self.circle_x, self.circle_y) return self.ball_img, def define_dx_dy(dx,dy,m,new_incli,old_incli): # 跳ね返ったときの x,y の増分 dx,dy を計算する関数である。 ## 渡された dx,dy は跳ね返る前の増分なので、まず、x,yの増分を old_dx,old_dyとして保存する。 old_dy = dy old_dx = dx # new_incli == infinity1 の場合は、跳ね返ったボールは、x軸に垂直に動くので、x の増分は0、 ## y の増分は上に向かって跳ね返ったときは負になり、下に向かって跳ね返ったときは正になる。 if new_incli == infinity1: dx = 0 if old_dy > 0: dy = -vertical_dy elif old_dy < 0: dy = vertical_dy ## y の増分が0、すなわちボールがx軸に平行に動くときは、x の増分が正 かつ 境界線の傾きが正のとき、または、 ## x の増分が負 かつ 境界線の傾きが負のときは、ボールは上に向かう。つまり、y の増分は正になる。 ## 逆に、x の増分が正 かつ 境界線の傾きが負のとき、または、x の増分が負 かつ 境界線の傾きが正のとき、 ## ボールは下に向かう。つまり、y の増分は負になる。これを、x の増分と境界線の傾きの積の符号によって判断する。 elif old_dy == 0: if old_dx * m > 0: dy = vertical_dy else: dy = -vertical_dy else: # y の増分は跳ね返った直線の傾きに、x の増分を掛ける。 dy = new_incli * incriment_x # 進入するボールはベクトルのイメージで(old_dx,old_dy)が方向ベクトルになる。 ## 跳ね返った後のボールの傾きはnew_incli であるが、どちらに進んでいいのかの判断が、 ## 煩雑であり、下にある条件文である。一つ一つのコメントは割愛する。 if old_dx > 0 and old_dy >= 0 and new_incli >= 0: if m >= 0: dx = abs(dx) dy = abs(dy) else: dx = -abs(dx) dy = -abs(dy) elif old_dx > 0 and old_dy >= 0 and new_incli < 0: if m >= 0: dx = -abs(dx) dy = abs(dy) else: if m >= new_incli: dx = abs(dx) dy = -abs(dy) else: dx = -abs(dx) dy = abs(dy) elif old_dx > 0 and old_dy < 0 and new_incli >= 0: if m >= 0: if m >= new_incli: dx = -abs(dx) dy = -abs(dy) else: dx = abs(dx) dy = abs(dy) else: if m >= old_incli: dx = abs(dx) dy = abs(dy) else: dx = -abs(dx) dy = -abs(dy) elif old_dx < 0 and old_dy >= 0 and new_incli >= 0: if m >= 0: if m >= new_incli: dx = abs(dx) dy = abs(dy) else: dx = -abs(dx) dy = -abs(dy) else: dx = abs(dx) dy = abs(dy) elif old_dx > 0 and old_dy < 0 and new_incli < 0: if m >= 0: dx = -abs(dx) dy = abs(dy) else: if m >= old_incli: dx = abs(dx) dy = -abs(dy) else: dx = abs(dx) dy = -abs(dy) elif old_dx < 0 and old_dy >= 0 and new_incli < 0: if m >= 0: dx = abs(dx) dy = -abs(dy) else: dx = -abs(dx) dy = abs(dy) elif old_dx < 0 and old_dy < 0 and new_incli >= 0: if m >= 0: dx = -abs(dx) dy = -abs(dy) else: dx = abs(dx) dy = abs(dy) elif old_dx < 0 and old_dy < 0 and new_incli < 0: if m >= 0: if m >= old_incli: dx = abs(dx) dy = -abs(dy) else: dx = -abs(dx) dy = abs(dy) else: if m >= new_incli: dx = -abs(dx) dy = abs(dy) else: dx = abs(dx) dy = -abs(dy) return dx,dy def update_an(i): # FuncAnimation に渡すための関数 ball_x, ball_y= ball.state_update() ball_img, = drawer.draw_circle(ball_x, ball_y) return ball_img, def incli_line(m,incli): # 跳ね返った直線の傾きを計算する関数 ## 入射角と反射角が等しいといことから、導かれる。 ## m が境界線の傾きで、incli が入射する直線の傾きである。 incli_limit = 1000 if incli == infinity1: new_incli = (1-m**2)/2*m else: denomi = 1+2*m*incli-m**2 if denomi != 0: new_incli = (2*m+incli*m**2-incli)/denomi else: new_incli = infinity1 if new_incli < -incli_limit or new_incli > incli_limit: new_incli = infinity1 return new_incli def distance(m,n,x,y): # ボールと境界線までの距離を計算する関数 dd = abs(m*x-y+n)/math.sqrt(m**2+1) return dd ############################################################# #########################__MAIN__############################ ############################################################# # infinity1=-99999 は、跳ね返った直線がx軸に対して垂直なったとき ## の傾きの値として使う。 infinity1=-99999 # 画面の初期設定で、この詳細はOh!Pythonの第1版を参照。 fig = plt.figure() ax = fig.add_subplot(111) ax.set_title("Oh!PYTHON Mathmatics 2", fontsize = 16) # 境界線を設定する。問題は3直線で囲まれた領域をボールが跳ねるので、 ## その3つの境界線の設定をここで行う。 ## 3境界線の設定 m=(直線1の傾き,直線2の傾き,直線3の傾き) ※x軸に垂直な直線は扱わない ## n=(直線1のy切片,直線2のy切片,直線3のy切片) m=(2,1/4,-3) n=(4,-3,18) # 領域の指定をする。 直線の上側は1、直線の下側は-1 とし、 ## さらに、line_side=(直線1の上下,直線2の上下,直線3の上下)とする。 ## この例は、境界線y=2x+4 の下側、y=1/3x-2の上側、y=-8/3x+18 の下側になる。 line_side = (-1,1,-1) # 3つの境界線の交点を求めて、グラフの範囲を計算する。 ## 交点は、sympy を使って求める。 sp.var('x,y') eq=[] ans=[] for i in range(3): eq.append(sp.Eq(y,m[i]*x+n[i])) ans.append(sp.solve([eq[0],eq[1]],[x,y])) ans.append(sp.solve([eq[0],eq[2]],[x,y])) ans.append(sp.solve([eq[1],eq[2]],[x,y])) # 3つの境界線の交点のx,y座標の最大値と最小値を求める。 max_x=max(ans[0][x],ans[1][x],ans[2][x]) min_x=min(ans[0][x],ans[1][x],ans[2][x]) max_y=max(ans[0][y],ans[1][y],ans[2][y]) min_y=min(ans[0][y],ans[1][y],ans[2][y]) # グラフの範囲は上で求めた最小値に-2、最大値に+2 をして多少広げる。 max_x_Axis = int(max_x)+2 min_x_Axis = int(min_x)-2 max_y_Axis = int(max_y)+2 min_y_Axis = int(min_y)-2 # グラフの範囲の設定をする。 ax.set_xlim(min_x_Axis, max_x_Axis) ax.set_ylim(min_y_Axis, max_y_Axis) # x軸、y軸をannotate を使って表示する。 ax.annotate("", xy = (max_x_Axis, 0), xytext = (min_x_Axis, 0), arrowprops = dict(shrink=0, width=1, headwidth=4, headlength=5, connectionstyle='arc3', facecolor='k', edgecolor='k')) ax.annotate("", xy = (0,max_y_Axis), xytext = (0,min_y_Axis), arrowprops = dict(shrink=0, width=1, headwidth=4, headlength=5, connectionstyle='arc3', facecolor='k', edgecolor='k')) # x,y軸及び原点Oに名前を設定する。 ax.text(max_x_Axis-0.8,-0.8,'x',fontsize=13) ax.text(-0.8,max_y_Axis-0.8,'y',fontsize=13) ax.text(-0.8,-0.8,'O',fontsize=13) # 座標平面にグリッドを青色で設定する。 ax.grid(which="major",axis="x",color="blue",alpha=0.8,linestyle="--",linewidth=1) ax.grid(which="major",axis="y",color="blue",alpha=0.8,linestyle="--",linewidth=1) # 座標平面の縦と横の比を等しくする ax.set_aspect('equal') # 境界線の定義域( x の範囲)を設定する。 x=np.arange(min_x_Axis,max_x_Axis + 1) # x,y の増加量の初期値を設定する。 ## incriment_x は x の基本の増加量,inciment_y は 最初のボールがどちらに向かうかの量 ## すなわち、incriment_y/incriment_x の値は、最初のボールの向かう直線の傾きを表す ## vertival_dy は x軸について垂直になったときの増加量 incriment_x = 0.2 incriment_y = 0.2 vertical_dy = 1 # 3つの境界線を描画する。 for i in range(3): y=m[i]*x+n[i] ax.plot(x,y,color="blue") # ボールの位置を計算するインスタンスを作成する。 ## Cordinate(xの初期値,yの初期値,直線の傾き,直線のy座標,領域,x の増加量,y の増加量) ball = Coordinate(3, 4, m, n, line_side,incriment_x,incriment_y) # ballで計算した位置に円を描画するインスタンスを作成する。 ## marks は軌跡の設定で、 True=足跡を付ける False=足跡を付けない となる。 marks = False drawer = Drawing(ax,marks) # アニメーションのライブラリに渡す animation = anima.FuncAnimation(fig, update_an, interval=5, frames=10) plt.show()