购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

2.3 代码

定义一个名为Spiro的类,用于绘制曲线。这个类可用于根据自定义参数绘制单条繁花曲线,也可用于在动画中同时绘制多条随机繁花曲线。为协调动画,定义另一个名为SpiroAnimator的类。在程序的顶层编写一个将图形存储为图像文件的函数,同时使用函数main()来接收用户输入并启动动画。

要查看完整的项目代码,可参阅2.7节“完整代码”,也可见本书配套源代码中的“/spirograph/spiro.py”。

2.3.1 绘制繁花曲线

Spiro类包含几个用于绘制繁花曲线的方法。下面先来看这个类的构造函数:

class Spiro:
    # 构造函数
    def __init__(self, xc, yc, col, R, r, l):
 
        # 创建turtle对象
      ❶ self.t = turtle.Turtle()
        # 设置光标形状
      ❷ self.t.shape('turtle')
        # 设置以度为单位的步长
      ❸ self.step = 5
        # 设置绘画结束标志
      ❹ self.drawingComplete = False
 
        # 设置参数
        self.setparams(xc, yc, col, R, r, l)
 
        # 开始绘画
        self.restart()

这个构造函数首先创建一个新的turtle对象❶,让每个Spiro对象都有一个与之相关联的turtle对象,这意味着可同时使用多个Spiro对象绘制一系列繁花曲线。接下来,将光标形状设置为海龟❷(还可将光标设置为其他形状,详情请参阅Python官方文档中的“turtle——海龟绘图”部分)。然后,将角度参数的递增量设置为5°❸,并创建布尔标志drawingComplete,用于表明繁花曲线是否已绘制完毕❹。同时使用多个Spiro对象绘制繁花曲线时,这个标志很有用,能够确定某条特定的繁花曲线是否已绘制完毕。这个构造函数在最后调用了接下来将讨论的两个设置方法。

1.设置方法

在Spiro类中,方法setparams()和restart()都用于在绘制繁花曲线前完成一些设置工作。下面先来看看方法setparams():

def setparams(self, xc, yc, col, R, r, l):
    #设置定义繁花曲线的参数
    self.xc = xc
    self.yc = yc
    self.R = int(R)
    self.r = int(r)
    self.l = l
    self.col = col
    # 通过除以GCD将分数约分
  ❶ gcdVal = math.gcd(self.r, self.R)
  ❷ self.nRot = self.r//gcdVal
    # 计算半径比
    self.k = r/float(R)
    # 设置颜色
    self.t.color(*col)
    # 存储当前角度
  ❸ self.a = 0

首先,存储了繁花曲线的中心坐标xc和yc。然后,将每个圆的半径R和r都转换为整数,并存储转换结果。另外,还存储了l和col,它们分别指定了笔尖的位置和繁花曲线的颜色。接下来,使用Python内置模块math中的方法gcd()计算两个半径的GCD❶,再使用这项信息确定繁花曲线的周期性,并将其存储在self.nRot中❷。最后,将角度参数a的初始值设置为0❸。

方法restart()也用于完成设置任务,它能重置Spiro对象的绘图参数,并将笔尖放到正确的位置,为绘画做好准备。这个方法可用来在程序的绘画部分复用同一个Spiro对象来依次绘制多条繁花曲线。

为绘制新的繁花曲线做好准备后,程序将调用方法restart(),这个方法的代码如下:

def restart(self):
    # 设置绘画结束标志
    self.drawingComplete = False
    # 显示海龟
    self.t.showturtle()
    # 移到起始位置
  ❶ self.t.up()
  ❷ R, k, l = self.R, self.k, self.l
    a = 0.0
  ❸ x = R*((1-k)*math.cos(a) + l*k*math.cos((1-k)*a/k))
    y = R*((1-k)*math.sin(a) - l*k*math.sin((1-k)*a/k))
  ❹ self.t.setpos(self.xc + x, self.yc + y)
  ❺ self.t.down()

首先将drawingComplete标志重置为False,指出Spiro对象,为绘制新的繁花曲线做好准备。然后,显示海龟光标,以防它被隐藏。接下来将画笔抬起❶,以免在❹处移到起始位置时绘制一条线段。❷处使用了一些局部变量,这旨在让代码更简洁。将这些变量代入计算繁花曲线的参数方程,以计算起点的坐标x和y❸,这是通过将角度参数a的初始值设置为0.0实现的。最后,将海龟移到正确的位置后放下画笔,以便能够开始绘制繁花曲线❺。

2.方法draw()

如果使用命令行参数来设置繁花曲线,这个程序将绘制指定的繁花曲线,这是使用Spiro类的方法draw()完成的。这个方法通过绘制一系列相连的线段一次性绘制整条繁花曲线:

def draw(self):
    # 绘制余下的线段
    R, k, l = self.R, self.k, self.l
  ❶ for i in range(0, 360*self.nRot + 1, self.step):
        a = math.radians(i)
      ❷ x = R*((1-k)*math.cos(a) + l*k*math.cos((1-k)*a/k))
        y = R*((1-k)*math.sin(a) - l*k*math.sin((1-k)*a/k))
 
        try:
          ❸ self.t.setpos(self.xc + x, self.yc + y)
        except:
            print("Exception, exiting.")
            exit(0)
    # 繁花曲线已绘制完毕,因此隐藏海龟光标
  ❹ self.t.hideturtle()

在这个方法中,遍历参数i的取值范围,即0到360与nRot的乘积❶。在这个循环中,根据参数i的当前值计算出相应点的坐标x和y❷,并调用turtle对象的方法setpos()绘制一条从前一个点到当前点的线段❸。调用这个方法的代码放在一个try块中,以便能够捕获可能出现的异常(如用户在绘图期间关闭了窗口),并妥善地退出。最后,隐藏光标❹,因为绘制工作已完成。

3.方法update()

如果没有使用命令行参数,这个程序将以动画方式绘制多条随机的繁花曲线,这要求对前面绘制繁花曲线的代码进行重构:不一次性绘制整条繁花曲线,而是编写一个只绘制繁花曲线中一条线段的方法,并在动画的每一步都调用这个方法。为满足这种需求,在Spiro类中定义了方法update(),如下所示:

def update(self):
    # 如果繁花曲线已绘制完毕,就跳过后面的步骤
  ❶ if self.drawingComplete:
        return
    # 递增角度
  ❷ self.a += self.step
    # 绘制一条线段
    R, k, l = self.R, self.k, self.l
    # 设置角度
  ❸ a = math.radians(self.a)
    x = self.R*((1-k)*math.cos(a) + l*k*math.cos((1-k)*a/k))
    y = self.R*((1-k)*math.sin(a) - l*k*math.sin((1-k)*a/k))
 
    try:
      ❹ self.t.setpos(self.xc + x, self.yc + y)
    except:
        print("Exception, exiting.")
        exit(0)
    # 如果繁花曲线已绘制完毕,就设置相应的标志
  ❺ if self.a >= 360*self.nRot:
        self.drawingComplete = True
        # 繁花曲线已绘制完毕,因此隐藏海龟光标
        self.t.hideturtle()

在这个方法中,首先检查是否设置了标志drawingComplete❶,如果否,就执行余下的代码。接着递增当前角度❷,根据当前角度计算相应点的坐标x和y❸,并将海龟移到该点,从而绘制一条线段❹。这些代码与方法draw()中for()循环内的代码一样,但只执行一次。

前面讨论繁花曲线的参数方程时,谈到了这种曲线的周期性。角度增加到一定程度后,繁花曲线将开始重复。在方法update()的最后,检查角度是否增大到了这样的程度❺,如果是,就设置标志drawingComplete,因为繁花曲线已绘制完毕。最后,隐藏海龟光标,以便欣赏漂亮的作品。

2.3.2 协调动画

SpiroAnimator类能用于以动画方式绘制多条随机的繁花曲线。这个类负责协调多个参数随机的Spiro对象,这是通过使用定时器定期调用每个Spiro对象的方法update()实现的。这种技术可定期地更新图形,还可让程序能够处理诸如按键、单击等事件。

下面先来看看SpiroAnimator类的构造函数:

class SpiroAnimator:
    # 构造函数
    def __init__(self, N):
        # 设置定时器值,单位为毫秒
      ❶ self.deltaT = 10
        # 获取窗口尺寸
      ❷ self.width = turtle.window_width()
        self.height = turtle.window_height()
        # 设置重新开始标志
      ❸ self.restarting = False
        # 创建Spiro对象
        self.spiros = []
        for i in range(N):
            #生成随机参数
          ❹ rparams = self.genRandomParams()
            #设置繁花曲线参数
          ❺ spiro = Spiro(*rparams)
            self.spiros.append(spiro)
        # 调用定时器
      ❻ turtle.ontimer(self.update, self.deltaT)

在SpiroAnimator类的构造函数中,将deltaT设置成了10❶,这是后面将用于定时器的时间间隔,单位为毫秒(ms)。接下来,存储了turtle窗口的尺寸❷,并初始化了一个标志❸,它用于指出是否正在重新开始绘制。在一个重复N次的循环中(N是在实例化SpiroAnimator对象时传入的),创建新的Spiro对象❺,并将它们添加到列表spiros中。创建每个Spiro对象前,都调用了辅助方法genRandomParams()❹(稍后将介绍这个方法)来随机地生成繁花曲线的参数。然而,Spiro类的构造函数要求向它提供多个参数,因此需使用Python运算符*将传入的元组拆分为一系列参数。最后,设置方法turtle.ontimer(),使其每隔deltaT毫秒就调用update()一次❻,以生成动画效果。

1.生成随机参数

创建每个Spiro对象时,都将向它传递使用方法genRandomParams()生成的随机参数,以生成各种各样的繁花曲线。每当Spiro对象绘制完一条繁花曲线,并为绘制新的繁花曲线做好准备后,都要调用这个方法。

def genRandomParams(self):
    width, height = self.width, self.height
    R = random.randint(50, min(width, height)//2)
    r = random.randint(10, 9*R//10)
    l = random.uniform(0.1, 0.9)
    xc = random.randint(-width//2, width//2)
    yc = random.randint(-height//2, height//2)
    col = (random.random(),
           random.random(),
           random.random())
  ❶ return (xc, yc, col, R, r, l)

为生成随机参数,使用了Python模块random中的3个方法:randint()、uniform()和random()。randint()随机地返回一个位于指定范围内的整数,uniform()随机地返回一个位于指定范围内的浮点数,而random()随机地返回一个位于0~1之间的浮点数。获取窗口宽度和高度中较小的那个值,并将其除以2,再将R设置为从50到这个计算结果之间的一个随机整数;将r设置为10到R的90%之间的一个随机整数;将l设置为0.1~0.9的一个随机浮点数。

接下来,在屏幕上随机地选择一个点作为繁花曲线的中心,即在屏幕边界内随机地选择 x y 坐标作为xc和yc。为繁花曲线随机地选择一种颜色,即为红色、绿色和蓝色分量分别选择一个随机值(这些值在范围0~1内)。最后,将所有参数作为一个元组返回❶。

2.重新开始

SpiroAnimator类有自己的方法restart(),这个方法用来重新开始,以绘制一组新的繁花曲线:

def restart(self):
    # 如果正在重新开始,就不重新开始
  ❶ if self.restarting:
        return
    else:
        self.restarting = True
    for spiro in self.spiros:
        # 清屏
        spiro.clear()
        # 生成随机参数
        rparams = self.genRandomParams()
        # 设置繁花曲线的参数
        spiro.setparams(*rparams)
        # 重新开始绘制
        spiro.restart()
    # 结束重新开始绘制过程
  ❷ self.restarting = False

这个方法遍历所有的Spiro对象,对于每个对象,都清除以前绘制的内容,并随机地生成一组新的繁花曲线参数。然后,使用Spiro对象的设置方法setparams()和restart()来设置参数,从而让Spiro对象为绘制下一个繁花曲线做好准备。标志self.restarting❶用于防止这个方法未执行完毕时被再次调用,例如当用户重复地按空格键时就可能出现这种情况。在方法restart()末尾重置了这个标志,以防忽略对方法restart()的下一次调用❷。

3.更新动画

定时器每隔10ms就会调用SpiroAnimator类的方法update()一次,以更新动画中使用的所有Spiro对象,这个方法的代码如下:

def update(self):
    # 更新所有的繁花曲线
  ❶ nComplete = 0
    for spiro in self.spiros:
        # 更新
      ❷ spiro.update()
        # 计算已绘制完毕的繁花曲线数
      ❸ if spiro.drawingComplete:
            nComplete += 1
    # 如果所有的繁花曲线都已绘制完毕,就重新开始
  ❹ if nComplete == len(self.spiros):
        self.restart()
    # 调用定时器
    try:
      ❺ turtle.ontimer(self.update, self.deltaT)
    except:
        print("Exception, exiting.")
        exit(0)

方法update()使用计数器nComplete来跟踪有多少个Spiro对象已完成绘制❶。它遍历Spiro对象列表,并调用这些对象的方法update()❷,以绘制每条繁花曲线的下一条线段。如果一个Spiro对象已完成绘制,就将计数器加1❸。

在循环外检查计数器,以确定是否所有Spiro对象都已完成绘制❹。如果都已完成,就调用方法restart(),使用全新的Spiro对象重新开始绘制。在方法update()的末尾,调用了模块turtle的方法ontimer()❺,以便在deltaT毫秒后再次调用update()。正是这些代码让动画得以继续下去。

4.显示/隐藏光标

在SpiroAnimator类中,下面的方法用于在显示和隐藏海龟光标之间切换。隐藏光标后,可提高绘画速度。

def toggleTurtles(self):
    for spiro in self.spiros:
        if spiro.t.isvisible():
            spiro.t.hideturtle()
        else:
            spiro.t.showturtle()

这个方法使用turtle内置的方法在隐藏和显示光标之间切换(如果当前可见就隐藏,如果当前不可见就显示)。在动画运行期间,当用户按T键时将调用方法toggleTurtles()。

2.3.3 保存曲线

好不容易生成繁花曲线后,能够保存结果就再好不过了。可使用独立函数saveDrawing()将绘图窗口的内容保存为PNG图像文件:

def saveDrawing():
    # 隐藏海龟光标
      ❶ turtle.hideturtle()
        # 生成独一无二的文件名
      ❷ dateStr = (datetime.now()).strftime("%d%b%Y-%H%M%S")
        fileName = 'spiro-' + dateStr
        print('saving drawing to {}.eps/png'.format(fileName))
        # 获取tkinter画布
        canvas = turtle.getcanvas()
        # 将图形保存为EPS文件
      ❸ canvas.postscript(file = fileName + '.eps')
        #使用模块Pillow将EPS文件转换为PNG文件
      ❹ img = Image.open(fileName + '.eps')
      ❺ img.save(fileName + '.png', 'png')
        # 显示海龟光标
        turtle.showturtle()

首先,隐藏了海龟光标❶,以防它出现在保存的图形中。然后,使用datetime()为图像文件生成基于时间戳的独一无二的名称(格式为“日-月-年-时-分-秒”)❷。为生成文件名,将这个字符串附加在spiro-后面。

这个海龟绘图法程序使用tkinter创建的用户界面(UI)窗口,因此使用tkinter的画布对象以EPS(Embedded PostScript,嵌入式PostScript)文件格式保存窗口中的图像❸。EPS是矢量图格式,可使用它以高分辨率输出图像,但PNG格式的用途更为广泛,因此使用Pillow打开EPS文件❹,再将其保存为PNG文件❺。最后,显示海龟光标。

2.3.4 分析命令行参数及初始化

本书的大多数项目都支持使用命令行参数进行定制。这里不使用人工来分析命令行参数(以防带来麻烦),而将这项烦琐的任务交给Python模块argparse去完成。在繁花曲线绘制程序的main()函数中,第一部分完成的就是命令行参数分析工作:

def main():
  ❶ parser = argparse.ArgumentParser(description=descStr)
 
    # 添加要求的参数
  ❷ parser.add_argument('--sparams', nargs=3, dest='sparams', required=False,
                        help="The three arguments in sparams: R, r, l.")
 
    # 分析参数
  ❸ args = parser.parse_args()

为管理命令行参数,创建了一个ArgumentParser对象❶。然后,在这个ArgumentParser对象中添加了参数--sparams❷,它由3部分组成,分别对应于繁花曲线的参数R、r和l。使用选项dest指定了对参数进行分析后应将得到的值存储在哪个变量中,并使用required=False指出参数--sparams是可选的。调用方法parse_args()来分析参数❸,通过对象args的属性访问参数。在这里,可通过args.sparams来访问参数--sparams的值。

注意

在本书的每个项目中,都将采用这种基本模式来创建和分析命令行参数。

接下来,用函数main()设置一些turtle参数:

    # 将绘图窗口的宽度设置为屏幕宽度的80%
  ❶ turtle.setup(width=0.8)
 
    # 将光标形状设置为海龟
    turtle.shape('turtle')
 
    # 将标题设置为“Spirographs!”
    turtle.title("Spirographs!")
    # 添加存储图形的按键处理程序
  ❷ turtle.onkey(saveDrawing, "s")
    # 开始侦听
  ❸ turtle.listen()
 
    # 隐藏海龟光标
  ❹ turtle.hideturtle()

使用setup()将绘图窗口的宽度设置为屏幕宽度的80%❶(还可向setup()传递高度参数和原点参数)。然后,将光标形状设置为海龟,并将程序窗口的标题设置为“Spirographs!”。接下来,使用onkey()让程序在用户按S键时调用函数saveDrawing()来保存图形❷。通过调用listen(),让绘图窗口侦听用户事件(如按键)❸。最后,隐藏海龟光标❹。

函数main()余下的代码如下:

    # 检查是否向--sparams发送了参数并绘制繁花曲线
  ❶ if args.sparams:
      ❷ params = [float(x) for x in args.sparams]
        # 使用给定的参数绘制繁花曲线
        col = (0.0, 0.0, 0.0)
      ❸ spiro = Spiro(0, 0, col, *params)
      ❹ spiro.draw()
    else:
        # 创建SpiroAnimator对象
      ❺ spiroAnim = SpiroAnimator(4)
        # 添加在显示/隐藏海龟光标之间切换的按键处理程序
        turtle.onkey(spiroAnim.toggleTurtles, "t")
        # 添加重新开始绘制的按键处理程序
        turtle.onkey(spiroAnim.restart, "space")
 
    # 开始turtle主循环
  ❻ turtle.mainloop()

首先检查是否给--sparams传递了参数❶,如果传递了,就只绘制这些参数定义的繁花曲线。这些参数是用字符串表示的,需要将它们解读为数字,因此使用列表推导式将它们转换为一个浮点数列表❷(列表推导式是一种Python结构,能够以紧凑而高效的方式创建列表。例如,a=[2*x for x in range (1, 5) ]创建一个列表,其中包含前4个正偶数)。然后,使用这些参数创建一个Spiro对象❸(这里借助了Python运算符*,它将列表拆分为一系列参数),并调用draw()来绘制相应的繁花曲线❹。

如果没有在命令行中指定参数,就进入随机动画模式。在这种模式下,创建一个SpiroAnimator对象❺并传入参数4,让程序同时绘制4条繁花曲线。然后,调用onkey()两次以捕获其他按键事件:按T键将调用方法toggleTurtles(),在显示和隐藏海龟光标之间切换;按空格键将调用restart()中断当前绘画,并开始绘制4条不同的随机繁花曲线。最后,调用mainloop()让tkinter窗口保持打开状态,以侦听事件❻。 FntbnNX4ptGp5xjfufoStNUcdBdfNDUrsndeoJETp4oOAARsA7HCh/kUzA6j6FnE

点击中间区域
呼出菜单
上一章
目录
下一章
×