Python 小技之繁花曲线

简介: Python 小技之繁花曲线

不知道大家有没有眼熟的感觉,反正是勾起了我不少回忆。


这种叫做“万花尺”的小玩意儿小时候应该不少人都玩过。一个大圆套一个小圆,圆与圆之间通过齿轮啮合在一起。


只需选中一个点,拿一支笔随着圆移动,就可以画出各种复杂的曲线,不同的曲线又可以进一步呈现出奇妙的图形。并且换用不同颜色的笔芯还可以使得图形更加丰富多彩(如上图所示)。这种图形还有一个更加文艺、好听的名字叫“繁花曲线”。


正好最近学到了 Python 的 turtle 模块,对于画这样的圆啊、曲线啊什么的再合适不过了。要用 Python 构建一个类似的图形,我们首先得要考察一下我们到底要画什么。


通过观察图形(当然也可以是通过观察万花尺的结构),我们可以很容易地发现:不论图形怎样变化,最终得到的图形大体上总是一个圆。换句话说,整个图形的大体框架就是一整个圆,其他的各种曲线都是在此基础上进一步曲折变换来的,因此我们第一步先要画一个圆。


turtle 画圆


仅仅是想用 turtle 模块来画圆的话很简单。

首先导入模块:


import turtle

生成画笔的实例,并调用已有的circle方法:


pen = turtle.Turtle()pen.circle(100)


微信图片_20220212164222.gif


这里方法的参数 100 指定的是所画圆的半径。此外该方法还有extentsteps两个参数,前者指定绘制圆的角度(单位为角度),后者我们放在后面来介绍。

我们还可以用这个方法来画一个太极图:


# pen = turtle.Turtle()pen.clear()pen.up() # 将画笔从画布上提起。即在画笔移动过程中不画出笔迹pen.setpos(0, -100) # 将画笔移动到画布的这个位置pen.down() # 将画笔放到画布上。即之后画笔的移动都会留下笔迹
# 画阴鱼pen.fillcolor("black")pen.begin_fill()pen.circle(100, 180)pen.end_fill()
# pen.fillcolor("black")pen.begin_fill()pen.circle(50, 180)pen.end_fill()
pen.fillcolor("white")pen.begin_fill()pen.circle(-50, 180)pen.end_fill()
# 画阳鱼pen.circle(-100, 180)
# 画阳眼pen.up()pen.setpos(0, 50-15)pen.down()# pen.fillcolor("white")pen.begin_fill()pen.circle(15)pen.end_fill()
# 画阴眼pen.up()pen.setpos(0, -50-15)pen.down()pen.fillcolor("black")pen.begin_fill()pen.circle(15)pen.end_fill()


微信图片_20220212164249.gif


画太极图


虽然太极图确实画出来了,但是可以发现,太极图本身结构并不复杂,而我们的代码需要把画笔多次抬起、放下,反复调用circle方法,十分繁琐。同时最致命的一个问题是,调用circle方法画出的图形一定是圆周或部分圆周,但我们真正要画的万花尺图形却并非都是由圆周曲线组成的,占大头的多是各种椭圆线、螺旋线。


所以到这里我们就遇到了一个问题:调用 circle方法,方便确实是方便,但真要想画出一条复杂的曲线,circle方法就无法给我们提供想要的灵活和自由度。


我们需要思考灵活度更高的绘图方法。


利用方程画图


还是画圆


上一小节我们讲到circle方法还有一个参数steps,但留了一个悬念没有说明参数的用途。


顾名思义,steps就是指“画圆的过程分为几步”。


实际上,circle方法并不是真的画出了一个完美的“圆”,而仅仅是使用多边形模拟的一个“近似的圆”,就像当年祖冲之计算圆周率用的方法一样。


知道了这一点,接下来就好办了。我们想画出一个圆也可以用这种方法,只要把圆周上的很多个点用线段连起来即可。


但是关键是首先要找出圆周上的若干个点。


我们可以回忆一下中学时代圆锥曲线的内容:圆的 x、y 两个坐标可以通过关于半径 r 和角度 θ 的两个参数方程分别确定。


用公式表达圆心在原点上的圆周坐标为:


image.png


先导入要用到的模块math


import math

这个模块中写好了cossin的函数实现,我们直接拿来用就好。

上述第二组表达式写成函数就是:



# 计算圆周的 x、y 坐标def cor_x_y(r, theta, a=0, b=0,):    # 把角度表示的 theta 转换为弧度表示。    # 因为 math 中的 cos 和 sin 都要求输入为弧度    rad = degreeToRadian(theta) # 该函数稍后实现        x = r * math.cos(rad) + a    y = r * math.sin(rad) + b        return (x, y)

其中用到了一个将角度转换为弧度的函数,实现起来也很简单:


def degreeToRadian(degree):    return degree * math.pi / 180

调用函数来画图试试:


pen.clear()
# 遍历圆周上的 180 个点for i in range(0,362,2):    if i != 0:        pen.setpos(*cor_x_y(100, i))    else:        pen.up()        pen.setpos(*cor_x_y(100, i))        pen.down()


微信图片_20220212164351.gif


用多边形近似画圆


完美!完全看不出跟真正圆的区别嘛哈哈~


画一条曲线


通过上面“以方画圆”的测试,我们可以得知用turtle模块描出轮廓点的方式可以非常好地模拟出圆形,自然而然别的曲线也不例外。


为了尽量减少本文中出现公式的频率,此处删减约一千字推导过程,于是我们得到了繁花曲线的坐标公式(来自Wikipedia,“Spirograph”词条):


image.png


其中,R 为大圆半径;r 为小圆半径;k 为小圆半径与大圆半径之比,即 r/R;ρ 为画笔到小圆圆心的距离;l(注意是小写字母L)为画笔到小圆圆新的距离与小圆半径之比,即 ρ/r;t 即对应坐标相对圆心的弧度。显然,k 和 l 都应该是介于 0 和 1 之间的实数。

上述公式实现如下:


# 用 d 表示画笔到小圆圆心的距离def cor_x_y_Spiro(R, r, l, theta):    k = r/R    ef = 1 - k        rad = degreeToRadian(theta)    x = R*(ef*math.cos(rad) + l*k*math.cos(ef/k*rad))    y = R*(ef*math.sin(rad) - l*k*math.sin(ef/k*rad))        return (x, y)


曲线重复的周期可以这样确定:将 k 化为最简分数,此时分子的大小 n 即为曲线的周期。也就是说我们只需遍历 n 个圆周即可画出闭合的曲线。


for i in range(0,360*3+2,2):    if i != 0:        pen.setpos(*cor_x_y_Spiro(100, 30, 0.6, i))    else:        pen.up()        pen.setpos(*cor_x_y_Spiro(100, 30, 0.6, i))        pen.down()


为了增加程序的灵活性,我们还需要实现一个函数用以求得这个周期数。而首先我们应当实现一个函数来求大圆半径和小圆半径的最大公约数(即 highest common factor,hcf)。这里我们用的是欧几里得算法,又称“辗转相除法”:


def hcf(x, y):    if x == y:        result = x    elif x > y:        result = hcf(x-y, y)    else:        result = hcf(x, y-x)        return result


用小圆半径除以该最大公约数,即可得到周期数:


def periods(R, r):    div = hcf(R, r)        return r//div


让我们把上面的代码封装一下:


import turtleimport math
class Spiro:    def __init__(self, R, r, l):        self.R = R        self.r = r        self.l = l        self.pen = turtle.Turtle()        
    def drawSingleSpiro(self):        # 周期数 p        p = self.periods()
        for i in range(0, 360*p + 2, 2):            if i != 0:                self.pen.setpos(*self.cor_x_y_Spiro(i))            else:                self.pen.up()                self.pen.setpos(*self.cor_x_y_Spiro(i))                self.pen.down()
    def hcf(self, x, y):        if x == y:            result = x        elif x > y:            result = self.hcf(x-y, y)        else:            result = self.hcf(x, y-x)        return result
    def periods(self):        div = self.hcf(self.R, self.r)        return self.r//div    
    def cor_x_y_Spiro(self, theta):        k = self.r/self.R        ef = 1 - k                rad = self.degreeToRadian(theta)        x = self.R*(ef*math.cos(rad) + self.l*k*math.cos(ef/k*rad))        y = self.R*(ef*math.sin(rad) - self.l*k*math.sin(ef/k*rad))            return (x, y)    
    def degreeToRadian(self, degree):        return degree * math.pi / 180
s = Spiro(100, 30, 0.6)s.drawSingleSpiro()turtle.mainloop()


微信图片_20220212164436.gif


两相切圆


这样就可以画出一条完整的曲线了。


完整实现


修改一下drawSingleSpiro方法的接口,增加一个参数pencolor来指定单条曲线的画笔颜色。同时去掉原Spiro类中的l属性——根据我们使用万花尺的经验,这一个参数应当是可变的——改为在某个随机处理函数中指定。


当然考虑到修改代码的方便性,本次修改仅仅是在随机处理函数中对l属性重新赋值。

最后,应当画多少条完整曲线也应由用户在初始化实例时自由指定。

代码如下:



import turtleimport mathimport random
class Spiro:    def __init__(self, R, r, l, num, color):        self.R = R        self.r = r        self.l = l        self.num = num        self.pen = turtle.Turtle()        self.pen.pencolor(color)        turtle.colormode(1.0)        
    def drawSingleSpiro(self):        # 周期数 p        p = self.periods()
        for i in range(0, 360*p + 2, 2):            if i != 0:                self.pen.setpos(*self.cor_x_y_Spiro(i))            else:                self.pen.up()                self.pen.setpos(*self.cor_x_y_Spiro(i))                self.pen.down()
    def drawWhole(self):        for s in range(self.num):            if s != 0:                self.randomSetting()                self.drawSingleSpiro()            else:                self.drawSingleSpiro()
    def randomSetting(self):        self.l = random.random()
        r = random.random()        g = random.random()        b = random.random()        self.pen.pencolor((r, g, b))
    def hcf(self, x, y):        if x == y:            result = x        elif x > y:            result = self.hcf(x-y, y)        else:            result = self.hcf(x, y-x)        return result
    def periods(self):        div = self.hcf(self.R, self.r)        return self.r//div    
    def cor_x_y_Spiro(self, theta):        k = self.r/self.R        ef = 1 - k                rad = self.degreeToRadian(theta)        x = self.R*(ef*math.cos(rad) + self.l*k*math.cos(ef/k*rad))        y = self.R*(ef*math.sin(rad) - self.l*k*math.sin(ef/k*rad))            return (x, y)    
    def degreeToRadian(self, degree):        return degree * math.pi / 180
s = Spiro(100, 30, 0.6, 5, "pink")s.drawWhole()turtle.mainloop()



image.png


小 tips

大圆半径与小圆半径尽量互质,得到的图形会更加复杂精巧哦~

示例代码:https://github.com/JustDoPython/python-100-day/tree/master/Spiro


目录
相关文章
|
7月前
|
Python
核密度曲线(python
核密度曲线(python
70 0
|
4月前
|
机器学习/深度学习 开发者 Python
Python 与 R 在机器学习入门中的学习曲线差异
【8月更文第6天】在机器学习领域,Python 和 R 是两种非常流行的编程语言。Python 以其简洁的语法和广泛的社区支持著称,而 R 则以其强大的统计功能和数据分析能力受到青睐。本文将探讨这两种语言在机器学习入门阶段的学习曲线差异,并通过构建一个简单的线性回归模型来比较它们的体验。
72 7
|
6月前
|
Python
Python学习笔记之Matplotlib模块入门(直线图、折线图、曲线图、散点图、柱状图、饼状图、直方图、等高线图和三维图的绘制)-2
Python学习笔记之Matplotlib模块入门(直线图、折线图、曲线图、散点图、柱状图、饼状图、直方图、等高线图和三维图的绘制)
|
6月前
|
数据可视化 开发者 Python
Python学习笔记之Matplotlib模块入门(直线图、折线图、曲线图、散点图、柱状图、饼状图、直方图、等高线图和三维图的绘制)-1
Python学习笔记之Matplotlib模块入门(直线图、折线图、曲线图、散点图、柱状图、饼状图、直方图、等高线图和三维图的绘制)
|
7月前
|
机器学习/深度学习 Python
【Python 机器学习专栏】混淆矩阵与 ROC 曲线分析
【4月更文挑战第30天】本文介绍了机器学习中评估模型性能的两种工具——混淆矩阵和ROC曲线。混淆矩阵显示了模型在不同类别上的预测情况,包括真正例、假正例、真反例和假反例,帮助评估模型错误类型和数量。ROC曲线则通过假正率和真正率展示了模型的二分类性能,曲线越接近左上角,性能越好。文章还提供了Python中计算混淆矩阵和ROC曲线的代码示例,强调它们在模型选择、参数调整和理解模型行为中的应用价值。
211 0
|
7月前
|
存储 Python
【python】——超市管理系统和用turtle动态画图(爱心和魔幻曲线)
【python】——超市管理系统和用turtle动态画图(爱心和魔幻曲线)
【python】——超市管理系统和用turtle动态画图(爱心和魔幻曲线)
|
7月前
|
数据可视化 数据挖掘 Python
Python中使用Matplotlib插件绘制曲线
Python中使用Matplotlib插件绘制曲线
148 0
|
7月前
|
机器学习/深度学习 数据可视化 Python
Scikit-Learn 中级教程——学习曲线
Scikit-Learn 中级教程——学习曲线
475 3
|
7月前
|
Python
洛伦兹曲线(python
洛伦兹曲线(python
67 0
|
7月前
|
定位技术 Python
Python中GDAL批量绘制多时相栅格遥感影像的像元时间序列曲线图
Python中GDAL批量绘制多时相栅格遥感影像的像元时间序列曲线图