Python高级动态绘图系统:复杂曲线的轨迹演示

Python高级动态绘图系统:复杂曲线的轨迹演示,第1张

文章目录
    • 更新绘图系统
    • 绘制摆线
    • 外摆线和心脏线
    • 内摆线与星形线
    • 箕舌线

源码地址: Python动态绘图,包括椭圆、双曲线、抛物线、摆线、心形线等

更新绘图系统

摆线是最简单的旋轮线,指圆在直线上滚动时,圆周上某定点的轨迹。

设圆的半径为 r r r,在x轴上滚动 x x x距离则意味着旋转了 x r \frac{x}{r} rx弧度,则其滚动所产生的摆线如下

这个曲线和此前的圆锥曲线又有区别,圆锥曲线在演示时只有一个变化的图形,而这里却有两个,即圆和轨迹都在运动。考虑到以后会出现的更多的运动图形,所以最好想一个实用性更广的画图代码。

首先,是把traceline这两个同是运动的曲线进行合并,同时用画图参数来规范。

其中,画图过程中至少有三个参数需要给定

{'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)costbcosba+bty=(a+b)sintbsinba+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=(ab)cost+bcosbabty=(ab)sintbsinbabt

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+y2ay=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')

欢迎分享,转载请注明来源:内存溢出

原文地址: http://www.outofmemory.cn/langs/873762.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-13
下一篇 2022-05-13

发表评论

登录后才能评论

评论列表(0条)

保存