源码地址: Python动态绘图,包括椭圆、双曲线、抛物线、摆线、心形线等
更新绘图系统
摆线是最简单的旋轮线,指圆在直线上滚动时,圆周上某定点的轨迹。
设圆的半径为 r r r,在x轴上滚动 x x x距离则意味着旋转了 x r \frac{x}{r} rx弧度,则其滚动所产生的摆线如下
这个曲线和此前的圆锥曲线又有区别,圆锥曲线在演示时只有一个变化的图形,而这里却有两个,即圆和轨迹都在运动。考虑到以后会出现的更多的运动图形,所以最好想一个实用性更广的画图代码。
首先,是把trace
和line
这两个同是运动的曲线进行合并,同时用画图参数来规范。
其中,画图过程中至少有三个参数需要给定
{'flag':'o-', # 画图时的线型
'lw':2, # 线宽
'flush':True} # 是否更新
在更改类之后,果然可以重新绘制椭圆,这个椭圆和之前没什么区别,但若不更新过焦点的那两条线,则会看到一个更加性感的椭圆
其绘图类改写如下
class drawAni():
# func为参数方程
def __init__(self,traceFuncs, para, txtFunc,
ts,xlim,ylim,figsize=(16,9),iniFunc=None):
self.funcs = traceFuncs # 画图代码
self.para = para # 画图参数
self.txtFunc = txtFunc
self.fig = plt.figure(figsize=figsize)
ax = self.fig.add_subplot(autoscale_on=False,
xlim=xlim,ylim=ylim)
ax.grid()
if iniFunc!=None: initFunc(ax)
self.N = len(para) # 轨迹个数
self.traces = [ax.plot([],[],p['flag'], lw=p['lw'])[0]
for p in para]
self.text = ax.text(0.02,0.85,'',transform=ax.transAxes)
self.initXY(self.N)
self.ts = ts #此为自变量
self.run(ts)
def initXY(self,N):
self.xs = [[] for _ in range(N)]
self.ys = [[] for _ in range(N)]
def animate(self,t):
if(t==self.ts[0]):
self.initXY(self.N)
for i in range(self.N):
x,y = self.funcs[i](t)
if self.para[i]['flush']==False:
self.xs[i].append(x)
self.ys[i].append(y)
self.traces[i].set_data(self.xs[i], self.ys[i])
else:
self.traces[i].set_data(x,y)
self.text.set_text(self.txtFunc(t))
return self.traces+[self.text]
def run(self,ts):
self.ani = animation.FuncAnimation(
self.fig, self.animate, ts, interval=5, blit=True)
plt.subplots_adjust(left=0.05, right=0.95, top=0.95, bottom=0.1)
plt.show()
def save(self,saveName):
self.ani.save(saveName)
绘图代码为
from aniDraw import *
a,b,c = 5,3,4
def traceFunc(theta):
return a*np.cos(theta), b*np.sin(theta)
def lineFunc(theta):
x,y = traceFunc(theta)
return [-c,x,c], [0,y,0]
def txtFunc(theta):
th = 180*theta/np.pi
x,y = traceFunc(theta)
lenL = np.sqrt((x+c)**2+y**2)
lenR = np.sqrt((x-c)**2+y**2)
txt = f'theta={th:.2f}\nlenL={lenL:.2f},lenR={lenR:.2f}\n'
txt += f'lenL+lenR={lenL+lenR:.2f}'
return txt
xlim,ylim = (-a,a), (-b,b)
ts = np.linspace(0,6.28,200)
funcs = [lineFunc, traceFunc]
tracePara = [
{'flag':'o-','lw':2, 'flush':False},
{'flag':'-','lw':1, 'flush':False}
]
an = drawAni(funcs, tracePara, txtFunc,
ts, xlim, ylim, (12,9))
an.save("test.gif")
绘制摆线
有了功能更全面的类,就可以绘制摆线图像了
所谓摆线,就是滚动的圆上某一点的轨迹,对于半径为 r r r的圆而言,其滚动 x x x距离,则其滚过的角度为 θ = x r \theta=\frac{x}{r} θ=rx,当角度为 2 π 2\pi 2π时,刚好滚了一圈,即 x = 2 π r x=2\pi r x=2πr。
对于圆心为 ( 0 , 1 ) (0,1) (0,1)上初始位置为 ( 0 , 0 ) (0,0) (0,0)的点而言,当圆滚过 x x x距离时,其位置变化为
x c = x + r cos ( x r ) y c = 1 + r sin ( x r ) \begin{aligned} x_c&=x+r\cos(\frac{x}{r})\\ y_c&=1+r\sin(\frac{x}{r}) \end{aligned} xcyc=x+rcos(rx)=1+rsin(rx)
其代码为
from aniDraw import *
r = 1
theta = np.arange(0,6.4,0.1)
xCircle0 = np.cos(theta)
yCircle0 = 1+np.sin(theta)
txtFunc = lambda x : f'x ={x:.1f}\n'
def getCircle(x):
return xCircle0+x, yCircle0
def getCycloid(x):
#由于是向右顺时针滚,所以角度为负
return x + r*np.cos(-x), 1 + r*np.sin(-x)
def getPoint(x):
return [x + r*np.cos(-x)], [1 + r*np.sin(-x)]
xlim,ylim=(1,10),(0,2)
ts = np.arange(0,10,0.1)
funcs = [getCircle, getPoint, getCycloid]
para = [
{'flag':'-','lw':1, 'flush':True},
{'flag':'o','lw':1, 'flush':True},
{'flag':'-','lw':1, 'flush':False}
]
an = drawAni(funcs, para, txtFunc, ts, xlim, ylim, (22,4))
如果选取圆内或圆外的一点描成轨迹,则为次摆线,圆内点的轨迹为短幅摆线,
反之则为长幅摆线
其绘图代码如下
from aniDraw import *
sin, cos = np.sin, np.cos
# rC为摆线圆半径, rR为滚动圆半径
rC,rR = 2, 1
ts = np.arange(0,6.4,0.1)
xIn0, yIn0 = rC*cos(ts), rR + rC*sin(ts)
xOut0, yOut0 = rR*cos(ts), rR + rR*sin(ts)
getX = lambda x,r : x + r*cos(-x/rR)
getY = lambda x,r : rR + r*sin(-x/rR)
circleIn = lambda x : (xIn0+x, yIn0)
circleOut = lambda x : (xOut0+x, yOut0)
cycIn = lambda x : (getX(x, rC), getY(x, rC))
cycOut = lambda x : (getX(x, rR), getY(x, rR))
txtFunc = lambda x : f'x ={x:.1f}\n'
def getPoint(x):
x0, y0 = cycOut(x)
x1, y1 = cycIn(x)
x2, y2 = x, rR
x3, y3 = 2*x2-x1, 2*y2-y1
x4, y4 = 2*x2-x0, 2*y2-y0
return [x0,x1,x2,x3,x4], [y0,y1,y2,y3,y4]
xlim,ylim=(1,20),(-1,3)
ts = np.arange(0,20,0.2)
funcs = [circleIn, circleOut, cycIn, getPoint]
para = [
{'flag':'-','lw':1, 'flush':True},
{'flag':'-','lw':1, 'flush':True},
{'flag':'-','lw':1, 'flush':False},
{'flag':'o-','lw':1, 'flush':True}
]
an = drawAni(funcs, para, txtFunc, ts, xlim, ylim, (22,4))
an.save('test.gif')
其中,rC
为生成摆线的圆的半径,rR
为在地上滚动的圆的半径。当
r
c
>
r
r
r_c>r_r
rc>rr时为长幅摆线,反之为短幅摆线。
根据摆线的定义,设圆心坐标为 ( t , a ) (t,a) (t,a),点距离圆心的距离为 λ a \lambda a λa,易得其参数方程为
x = a ( t − λ sin t ) y = a ( 1 − λ cos t ) \begin{aligned} x = a(t-\lambda\sin t)\\ y = a(1-\lambda\cos t) \end{aligned} x=a(t−λsint)y=a(1−λcost)
随着 λ \lambda λ的变化,图像的变化过程为
外摆线和心脏线
如果在一个圆绕着另一个固定的圆滚动,如果在圆外滚动,则动圆上的某相对固定点的轨迹为外摆线;若在圆内滚动,则某点的轨迹为内摆线。设定圆半径为 r i r_i ri,动圆半径为 b b b,则绕行旋转 t t t度后,动圆圆心圆心位置为
( a ± b ) cos t , ( a ± b ) sin t (a\pm b)\cos t, (a\pm b)\sin\ t (a±b)cost,(a±b)sin t
动圆上固定点相对动圆圆心旋转的角度为
a ± b b t \frac{a\pm b}{b}t ba±bt
如选点 ( a , 0 ) (a,0) (a,0)作为起点,则外摆线的参数方程为
x = ( a + b ) cos t − b cos a + b b t y = ( a + b ) sin t − b sin a + b b t \begin{aligned} x = (a+b)\cos t-b\cos\frac{a+b}{b}t\\ y = (a+b)\sin t-b\sin\frac{a+b}{b}t \end{aligned} x=(a+b)cost−bcosba+bty=(a+b)sint−bsinba+bt
a = b a=b a=b时就得到了著名的心脏线,被许多直男奉为经典
其代码为
from aniDraw import *
sin, cos = np.sin, np.cos
rIn,rOut = 1, 1 #rIn为定圆半径, rOut为动圆半径
ts = np.arange(0,6.4,0.1)
# 初始化图形
def initFunc(ax):
ax.plot(rIn*cos(ts),rIn*sin(ts),'-',lw=1)
ax.axis('off')
FLAG = False #FLAG为True时表示内摆线,否则表示外摆线
rD = rIn-rOut if FLAG else rIn+rOut
# 文字说明
txtFunc = lambda t : f'theta ={t:.1f}\n'
# 摆线圆心
getCenter = lambda t : (rD*cos(t), rD*sin(t))
# 更新动圆位置
def updateCir(t):
xC, yC = getCenter(t)
return xC+rOut*cos(ts), yC+rOut*sin(ts)
# 更新摆线点
def getCyc(t):
xC, yC = getCenter(t)
th = rD/rOut*t
if FLAG: return xC+rOut*cos(th), yC-rOut*sin(th)
else : return xC+rOut*cos(th), yC+rOut*sin(th)
def getPoint(t):
x0, y0 = getCyc(t)
x1, y1 = getCenter(t)
return [x0,x1,2*x1-x0], [y0,y1,2*y1-y0]
xlim = ylim = (-rIn, rIn) if FLAG else (-rIn-2*rOut,rIn+2*rOut)
figsize = (5,5)
ths = np.arange(0,10,0.1)
funcs = [updateCir, getPoint, getCyc]
para = [
{'flag':'-','lw':1, 'flush':True},
{'flag':'o-','lw':1, 'flush':True},
{'flag':'-','lw':1, 'flush':False}
]
an = drawAni(funcs, para, txtFunc, ths, xlim, ylim, figsize, initFunc)
an.save('test.gif')
如果更改 a b \frac{a}{b} ba比值,则可得到
a
b
=
2
\frac{a}{b}=2
ba=2 |
a
b
=
1
2
\frac{a}{b}=\frac{1}{2}
ba=21 |
a
b
=
5
\frac{a}{b}=5
ba=5 |
a
b
=
2
3
\frac{a}{b}=\frac{2}{3}
ba=32 |
对 a b \frac{a}{b} ba进行约分得到 m n \frac{m}{n} nm,曲线由 m m m支组成,总共绕定圆 n n n周,然后闭合。观察 1 b = 1 2 \frac{1}{b}=\frac{1}{2} b1=21时的曲线,可以看到其前 π \pi π个值和后 π \pi π个值组成的心形更好看。
内摆线与星形线
当动圆在定圆内部转动时,则为内摆线,其方程为
x = ( a − b ) cos t + b cos a − b b t y = ( a − b ) sin t − b sin a − b b t \begin{aligned} x = (a-b)\cos t+b\cos\frac{a-b}{b}t\\ y = (a-b)\sin t-b\sin\frac{a-b}{b}t \end{aligned} x=(a−b)cost+bcosba−bty=(a−b)sint−bsinba−bt
a
b
=
2
\frac{a}{b}=2
ba=2 |
a
b
=
4
\frac{a}{b}=4
ba=4 |
a
b
=
5
\frac{a}{b}=5
ba=5 |
a
b
=
1
3
\frac{a}{b}=\frac{1}{3}
ba=31 |
当 a b = 4 \frac{a}{b}=4 ba=4时,其方程可化简为
x = a cos 3 t y = a sin 3 t x = a\cos^3t\quad y = a\sin^3t x=acos3ty=asin3t
被称为星形线。
内摆线和外摆线同常规的摆线一样,皆具有对应的长辐或短辐形式,其标准方程为
x = ( a + b ) cos t − λ cos a + b b t x = ( a + b ) sin t − λ sin a + b b t x = (a+b)\cos t-\lambda\cos\frac{a+b}{b}t\\ x = (a+b)\sin t-\lambda\sin\frac{a+b}{b}t\\ x=(a+b)cost−λcosba+btx=(a+b)sint−λsinba+bt
当 b > 0 b>0 b>0时为外摆线, b < 0 b<0 b<0时为内摆线,对于星形线而言,其变化过程为
箕舌线
过原点的动直线交定圆 x 2 + y 2 − a y = 0 , a > 0 x^2+y^2-ay=0, a>0 x2+y2−ay=0,a>0于P点,交直线 y = a y=a y=a于Q点,过P和Q分别作X轴和Y轴的平行线交于M点,则M点的轨迹叫做箕舌线 。
设 a = 2 a=2 a=2,则圆的参数方程为
x = cos θ , y = sin θ + 1 x=\cos\theta,y=\sin\theta+1 x=cosθ,y=sinθ+1
设动直线的方程为 y = k x y=kx y=kx,则随着 k k k的变化,可以得到一条箕舌线
这个图中,有一条直线和一个圆是固定不动的,故而放在初始化函数中。剩下的曲线又分为两类,一个是箕舌线需要动态生成,另外三个则是箕舌线的辅助线,需要实时变化。
绘图代码为
from aniDraw import *
cos, sin, tan = np.cos, np.sin, np.tan
r,a = 1,2 #分别为圆半径和直线纵坐标
xlim, ylim = (-5,5), (-0.1,r*2+0.5)
ts = np.arange(0,6.4,0.1)
def initFunc(ax):
ax.plot(r*cos(ts), r+r*sin(ts),'-',lw=1)
ax.plot(xlim, [a, a])
# 返回 px, py, qx
def getPQ(th):
k = tan(th)
px = 2*k/(k**2+1)
return px, k*px, a/k
def getWitch(th):
_, py, qx = getPQ(th)
return py, qx
txtFunc = lambda th : f'k ={tan(th):.1f}\n'
kLine = lambda th : ([0,a/tan(th)], [0,a])
def yLine(th):
_, py, qx = getPQ(th)
return [qx,qx],[2,py]
def xLine(th):
px, py, qx = getPQ(th)
return [px,qx],[py,py]
funcs = [getWitch, xLine, yLine, kLine]
para = [{'flag':'-', 'lw':1, 'flush':True} for _ in range(4)]
para[0] = {'flag':'-','lw':2, 'flush':False}
ks = np.arange(0,np.pi,0.05)
figsize = (15,4)
an = drawAni(funcs, para, txtFunc, ks, xlim, ylim, figsize, initFunc)
an.save(f'test.gif')