ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Python Turtle图标复制实战:从坐标建模到圆角矩形绘制

Python Turtle图标复制实战:从坐标建模到圆角矩形绘制

很多人私信问我,“Python 图标复制(Turtle)”到底是怎么玩的。这里的“图标复制”不是 Ctrl+C / Ctrl+V,而是靠 Python 自带的 Turtle 海龟绘图模块,把一个图标从视觉层面拆成坐标点、线条、圆弧和填充色,再用代码一笔一画重绘出来。说白了,就是从“看图”到“读坐标”再到“写指令”的过程。

这篇文章就把整个过程拆开讲清楚。你会看到我如何把一个播放按钮图标拆成圆角矩形和三角形,如何在坐标系里一步步定位置,以及遇到圆角方向反了、填充区域缺一块这类问题时的排查思路。内容没有复杂到什么程度,适合刚接触 Turtle 的人,也适合那些已经能画直线但不会画“像样图形”的同学。

1. 先把“复制图标”这件事想清楚

1.1 图标复制不是截图,是坐标建模

大部分图标看上去精致,拆到底其实就几类基础几何元素:矩形、圆角矩形、三角形、圆、圆弧、直线。所谓复刻图标,第一步永远不是写代码,而是做拆图。

举个例子,一个常见的播放按钮图标,外形是圆角矩形,内部放一个向右的三角形。就这么简单。难的地方在于:圆角矩形的圆角半径多大,内部三角形底边和高分别是多少,三角形放在矩形的哪个位置视觉上才不偏。这些参数确定之后,代码就是机械工作。

我自己习惯的做法是拿一张带网格的坐标纸,或者干脆在绘图软件里叠一个网格,把图标的关键点标成坐标。比如矩形左上角、圆角圆心、三角形三个顶点。坐标一出来,图形就成功了一半。这个习惯比任何技巧都重要,因为 Turtle 绘图的本质就是“按坐标走”。

1.2 为什么选 Turtle,而不是直接用图像库

有人可能会问,既然要复制图标,直接用 Pillow 加载图片、再保存出来不就行了?那样做只是复制了一张位图,不是“绘制”。用 Turtle 复刻图标的意义在于:保存下来的是一套绘制指令,它可以随时修改尺寸、调整颜色、换位置,也可以从一套代码派生出不同风格的同款图标。

Turtle 的另一个好处是过程可视化。OpenCV 和 Pillow 可以做图像处理,但你很难直观看到一条线从无到有的过程。Turtle 不同,你运行代码时能看到海龟在画布上移动,哪条线坐标不对,一眼就能发现。这种“所见即所得”的调试体验,对学习坐标计算非常有帮助。

2. 环境准备与 Turtle 高频操作

2.1 导入模块与画布设置

Python 3 自带 Turtle 模块,不需要额外安装,直接import turtle就能用。如果你在命令行里运行的是python xxx.py,只要编辑器支持图形窗口,运行后就会弹出一个画布窗口。

画布设置有三个常用函数:

import turtle turtle.setup(width=800, height=600) # 窗口大小,单位像素 turtle.title("Icon Copy Demo") # 窗口标题 turtle.bgcolor("#f0f0f0") # 背景颜色

Turtle 默认坐标原点在画布正中间,水平向右是 x 轴正方向,垂直向上是 y 轴正方向。这个坐标系和数学课上用的坐标系一致,计算坐标时直接用平时熟悉的几何思维就行。

如果图标比较大,担心超出窗口范围,可以用turtle.setworldcoordinates()重新定义坐标系范围。不过初学者不建议一开始就动它,先用默认坐标系,把图标尺寸控制在 400×400 以内,基本不会出界。

2.2 你只需要这 10 个画笔操作

Turtle 的 API 很多,实际画图标高频用到的,其实不超过下面这些:

操作写法用途
前进turtle.forward(100)画直线
后退turtle.backward(50)往回画线
左转turtle.left(90)改变方向
右转turtle.right(45)改变方向
移动到指定点turtle.goto(x, y)精确画坐标点
抬笔turtle.penup()移动时不画线
落笔turtle.pendown()开始画线
填充turtle.begin_fill()/turtle.end_fill()封闭区域填色
画圆弧turtle.circle(radius, extent)画圆或圆弧
颜色和粗细turtle.color("blue")/turtle.pensize(3)控制描边

其中最容易忽略的是penup()和pendown()。很多人的图形里出现莫名其妙的斜线,就是因为移动画笔时忘抬笔。落笔状态下一旦goto到远处,海龟会直接划出一道斜线。

3. 动手复刻一个播放按钮图标

3.1 步骤一:把图标拆成几何块

这里我选播放按钮作为示例,理由是它足够典型,既有直线又有圆角,还涉及两个图形的叠加,很适合讲解坐标布局。

拆图结果只有两部分:

  • 外框:一个圆角矩形,宽 220 像素,高 150 像素,圆角半径 25 像素。
  • 内部:一个填充为白色的三角形,代表“播放”符号。

关键是确定参数关系。圆角矩形宽 220、高 150、圆角半径 25 时,上下两条水平边的直线部分长度等于220 - 2 * 25 = 170,左右两条垂直边的直线部分长度等于150 - 2 * 25 = 100。这个计算是画圆角矩形的核心,千万不要直接把 220 当作直线长度画。

三角形的坐标也要单独设计。播放按钮的三角形底边在左侧,尖角指向右侧。为了让三角形在矩形中视觉居中,底边两个点一般放在距矩形左边缘约三分之一处,尖角放在距矩形右边缘约三分之一处。我选取的三个顶点是:

A = (-10, 35) B = (-10, -35) C = (50, 0)

三角形底边长度 70 像素,高度 60 像素。放在宽 220 的矩形里不会显得拥挤。

3.2 步骤二:定坐标,写代码

整个图标的绘制可以分成三步:画外框、画三角形、填充颜色。

画圆角矩形的关键是把起点定在左上角水平直线与圆弧的接合处。以默认中心点为原点计算:

W, H, R = 220, 150, 25 left_start_x = -(W / 2 - R) # -85 top_start_y = H / 2 # 75

起点坐标是(-85, 75)。从这里向右画 170 像素,到达(85, 75),然后画右上角圆弧。这里用到turtle.circle(-R, 90),负半径表示圆弧圆心在当前行进方向的右侧,正好画出一个凸向右下角的四分之一圆。后续每个圆角都遵循同样逻辑。

完整代码起来是这样的:

import turtle W, H, R = 220, 150, 25 screen = turtle.Screen() screen.title("Play Button Icon") t = turtle.Turtle() t.speed(8) t.pensize(2) t.color("#1a73e8") # 画外框圆角矩形 t.penup() t.goto(-(W/2 - R), H/2) t.pendown() t.setheading(0) t.forward(W - 2 * R) # 上边直线 t.circle(-R, 90) # 右上圆角 t.forward(H - 2 * R) # 右边直线 t.circle(-R, 90) # 右下圆角 t.forward(W - 2 * R) # 下边直线 t.circle(-R, 90) # 左下圆角 t.forward(H - 2 * R) # 左边直线 t.circle(-R, 90) # 左上圆角 # 画内部三角形 t.fillcolor("white") t.begin_fill() t.penup() t.goto(-10, 35) t.pendown() t.goto(-10, -35) t.goto(50, 0) t.goto(-10, 35) t.end_fill() t.hideturtle() screen.mainloop()

这里建议大家注意一个细节:三角形路径最后必须回到起点(-10, 35),end_fill()才能正确填充。如果只画到第三个点就结束,Turtle 虽然会自动闭合路径,但某些情况下填充边缘会出现缺角,尤其当图形复杂时。老老实实画回起点最稳妥。

3.3 步骤三:加颜色和填充

填充颜色的逻辑很简单,fillcolor设置填充色,color同时设置画笔颜色和填充色,begin_fill()和end_fill()之间绘制的内容就是填充区域。

外框和三角形是分开填充的。外框先画一圈圆角矩形,但不填充,或者填充成蓝色。三角形再填白色,覆盖在蓝色外框内部,形成典型播放按钮配色。

如果你想要边框线条更粗,可以执行t.pensize(3);想要描边更柔和,可以换成灰色系颜色。我建议对于图标复制这类型任务,先用单一颜色把结构画准,再考虑配色方案。结构不对,颜色越花越明显。

4. 调试排障与绘图习惯养成

4.1 图标画乱了,先查 4 个地方

实际写代码时,第一次跑出来的图形几乎一定不会完美。这时候不要慌,按下面顺序排查:

第一,看坐标是否符合预期。在关键点位置加一句turtle.write((x, y)),绘制时屏幕上就会显示当前坐标。这招比“肉眼猜测”高效太多了。我之前画一个齿轮图标,画到最后发现齿数不对,就是靠这个办法定位到第 7 个齿的位置计算有误。

第二,查方向设置。Turtle 默认方向是朝右,left(90)以后才朝上。很多人习惯性认为left(90)是朝左,这是新手最容易踩的坑。尤其在画圆弧时,方向错了整个图形都会变。

第三,查圆角方向。circle(-R, 90)画出来是顺时针方向的圆弧,circle(R, 90)是逆时针方向。如果你发现圆角凹进去了,把半径从负改成正就行。这不是玄学,是符号语义。

第四,查填充区域是否闭合。如果填充颜色“溢出来”,大概率是路径没闭合。在begin_fill()之前,先用penup()和pendown()确认当前位置,再检查最后一个点是否回到起点。

4.2 两个让绘制速度快两倍的设置

当图形线条多、圆角多时,Turtle 的逐笔绘制会显得很慢。有两个经典设置可以提速:

t.speed(0) # 设置为最快速度 turtle.tracer(0) # 关闭逐帧刷新 turtle.update() # 最后统一刷新

speed(0)表示海龟移动速度不限制,但依然每一笔都实时刷新。tracer(0)会暂停自动刷新,直到你调用update()才会一次性画完。这个组合在画复杂图标时几乎必备。我画一个 30 多步的齿轮图标,关闭刷新之前要等十几秒,关闭之后一秒内出图。

不过提速之后,绘制过程不可见,如果你需要观察每一步的路径,就把tracer(0)注释掉,放慢速度慢慢看。

4.3 保存图标:从窗口到图片文件

Turtle 默认不提供直接保存 PNG 的函数,但可以保存为 EPS 矢量图,再转换格式。保存 EPS 的代码非常简单:

canvas = turtle.getcanvas() canvas.postscript(file="play_button.eps")

EPS 文件可以用支持矢量图读取的工具打开,也可以转换成 PNG 或 PDF。如果你在项目里需要进一步处理,这一步是必须掌握的。

提示:postscript生成的是矢量图,缩放不模糊。但只有当前画布上绘制的内容会被保存,隐藏的海龟不会出现在图中。

5. 还能玩出什么花样

5.1 参数化:一套代码画多个尺寸

图标复刻做到后面,你会希望同一个图标能适配不同尺寸。比如小程序图标、网页按钮图标、文章封面小图,尺寸需求都不一样。这时可以把宽、高、圆角半径、三角形边长都写成函数参数。

def draw_play_button(width=220, height=150, radius=25): triangle_height = height * 0.45 triangle_start_x = -width / 4 triangle_vertex_x = width / 4 # 以下是绘制逻辑

这样调用draw_play_button(300, 200, 30)就能生成不同比例的播放图标。参数化之后,调整一个数值,整套坐标自动适配。这是从“画一个图标”升级到“画一类图标”的关键一步。

5.2 从位图到海龟指令:进阶复刻路径

如果目标图标不是简单几何图形,而是复杂的 Logo,或者是从网上下载的小图标,手动量坐标就不太现实了。有一个进阶思路可以试试:先用图像处理库把图片边缘提取出来,再做轮廓点采样,最后把点集转成 Turtle 的goto指令。

流程大体如下:

  1. 图像二值化,把背景变成纯色。
  2. 提取轮廓坐标点。
  3. 对坐标点做抽稀,减少冗余点。
  4. 生成 Python 文件,内容是一串goto调用。

这个方案我实际试过,效果取决于原图的清晰度和轮廓抽稀参数。抽稀太密,海龟画起来很慢;抽稀太粗,图形失真明显。我的经验是控制在一百到三百个点之间,视觉上既能保持轮廓,绘制速度也能接受。

这种思路虽然涉及 OpenCV,但核心依然是坐标思维:原图上的每个像素点,最终都要映射到海龟画布坐标上。从这个角度看,标题里“图标复制”的本质更加清晰了,它不是在复制文件,而是在翻译坐标。

我个人在实际操作中最喜欢的一个细节是:画图标前先画一个浅灰色的辅助网格,网格线不要太粗,够参考就行。这样每个坐标点都能快速在脑子里对位。网格画完马上隐藏,等成品出来时画面干干净净。这个方法帮我省了很多来回试坐标的时间。如果你也在练 Turtle 画图标,不妨先画一个 20×20 像素间隔的参考网格,跑通这个流程,再去复刻复杂图形就会顺手很多。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进