Python-可视化学习指南-全-

Python 可视化学习指南(全)

原文:zh.annas-archive.org/md5/a9851289669a8ca6a7e307f8cc1313d5

译者:飞龙

协议:CC BY-NC-SA 4.0

序言

当我第一次接触编程代码时,我盯着屏幕上的晦涩命令和符号,既惊讶又困惑,想知道怎么可能有人能理解这些,更别说写它了。我按下 F5 键,程序神奇地生成了一个城市景观,其中两个玩家以大猩猩形象互相投掷爆炸香蕉。我尝试修改几行代码看看会发生什么,有时结果是可以预见的或者很酷的。更多时候,游戏干脆无法运行。计算机为了“帮助”我,诊断出了我的错误,喋喋不休地讲解语法和各种“非法”操作。

在接下来的几年里,我一直满足于避免学习编程。然而,当我开始对使自己的创作更具互动性产生兴趣时,情况开始发生变化。你可能已经遇到了我曾经感到沮丧的那些障碍。也许你一直用视觉工具得心应手,但突然遇到瓶颈。或者你失望(甚至惊恐地发现)自己所想要实现的目标,必须深入到代码中去。

软件应用程序及其各种图形控件让我们感觉自己掌控一切。然而,当你发现缺少了你想要的工具时,这种幻觉很快就会消失。通过学习编程,你将真正掌握电脑的使用。

这本书适合谁?

本书假设读者没有编程经验。它力求将编程学习过程做到尽可能直观和有趣。内容基于我丰富的教学经验,特别是面向初学者、设计师和互动媒体专业学生。你将获得的技能和知识是编程的基础,适用于日益扩展的创意技术领域,如游戏、网页、增强/虚拟现实,甚至电影的视觉特效。

如果你是艺术家、学生、设计师、研究员,或者只是有兴趣学习编码技能的人,那么 Processing 的 Python 模式非常适合在视觉环境中学习编程。

对于有编程经验的人,本书可以帮助学习 Python、Processing 中的 Python 模式,或是创意编码技巧。

你可能已经接触过其他可视化编程语言——类似 Scratch,你通过连接图形元素(如框、图标和箭头)来编程。Python 并非这样的语言——它是一种文本编程语言,需要你手动输入代码。不过,为了使学习过程更具视觉性,你将专注于编写代码,生成图形、模式、动画、数据可视化、用户界面和模拟。这种方法不仅能创建出酷炫的图形,同时也帮助你更好地理解编程的基础概念。

什么是 Processing 的 Python 模式?

Processing 的 Python 模式结合了Python编程语言和Processing,这是一个用于交互式和图形编程的开发环境。你还会看到 Processing 的 Python 模式被称为 Processing.py。该项目最初是一个名为 Processing.py 的命令行工具,但其开发者决定在该工具为 Processing 开发环境提供支持时将其称为 Python 模式。在本书中,你可以认为这些术语是可以互换的。

Python 是目前最流行的编程语言之一。之所以如此流行有很多理由,下面是应该关注的原因。首先,Python 是一种适合初学者的编程语言。它比 Java 或 C++ 等语言更易于上手,因此你会发现它更容易阅读、编写和理解。其次,它是一种通用编程语言,适用于人工智能(AI)、游戏、仿真、Web 应用程序以及几乎所有其他领域。

Processing 自 2000 年代初以来就存在,它由一个编程语言和一个用于编写和编译代码的编辑器组成。它提供了一系列特殊命令,允许你通过代码绘图、制作动画并处理用户输入。其创始人 Casey Reas 和 Ben Fry 开发 Processing 旨在让编程对设计师和艺术家更为易用,尽管它如今的用户群体已经扩展到研究人员、爱好者和教育工作者。

Java 是最初的 Processing 编程语言的基础,但自那以后出现了其他变体,包括 JavaScript (p5.js) 和 Ruby (JRubyArt) 版本。2010 年,Jonathan Feinberg 创建了 Processing.py,你可以将其看作是 Processing 的一种扩展,它允许你使用 Python 编写代码,而不是类似 Java 的代码。

Python 和 Processing 都是开源的,且不收费。更重要的是,你几乎可以在任何平台上使用它们,包括 Linux、macOS 和 Microsoft Windows。

什么是算法?

你在编程领域中将经常遇到“算法”这个词。你可以将算法看作是计算机或机器为了达到特定目标必须遵循的一组规则。例如,制作一杯速溶咖啡的算法可以如下所示:

  1. 在杯子里放一茶匙咖啡粉。

  2. 将水壶加满水。

  3. 打开水壶。

  4. 当水烧开后,将 240 毫升沸水倒入杯中。

  5. 向同一杯子中加入一平茶匙的糖。

  6. 搅拌内容。

  7. 上菜。

然而,这一系列步骤不足以编程一个真实的咖啡制作机器人。如果杯子的大小不同,小杯子可能会溢出。此外,机器人也会忽视任何加奶或加糖的请求。计算机无法做出任何假设,必须要求明确无歧义的指令,并使用机器理解的语言来沟通—例如 Python。学习 Python 语言可能是你最初遇到的障碍,但随着你逐渐熟练,挑战将转向算法思维的掌握。

什么是创意编码?

创意编码是用于创意输出的计算机编程。这个广泛的术语包含但不限于计算机生成的音频和视觉艺术、互动装置、实验性游戏和数据可视化。

以 Frederic Brodbeck 的Cinmetrics项目为例。Brodbeck 使用 Python 开发了一个程序,分析 DVD 电影数据以生成电影的视觉指纹。指纹是由许多片段组成的开放环形图;每个片段代表 10 个镜头的跨度,同心圆带显示了每个片段的颜色分解。每个片段的对角线长度表示运动量。图 1 是电影量子危机(2008)的Cinmetrics指纹。

f00001

图 1:量子危机的指纹,由 Frederic Brodbeck 创建。截图来自cinemetrics.site/

这些指纹还可以动画化,此时,运动将通过脉动的片段来呈现。互动界面提供了预设和滤镜的选择,以便你可以将指纹排列在一起进行比较—例如,比较原版与翻拍、不同类型的电影、同一导演的作品等等。图 2 比较了(从左到右)2001 太空漫游(1968)、辛普森电影(2007)和一场足球比赛。

f00002

图 2:比较(从左到右)2001 太空漫游辛普森电影和一场足球比赛的指纹。截图来自cinemetrics.site/

许多创意编码项目采用类似的基础方法,其中数据被输入到程序中以影响它如何控制输出。实时音频同步的音乐可视化—如在流行媒体播放器软件中展示的那样—就是一个很好的例子。然而,你还可以尝试其他许多数据源,如网络订阅源、健身追踪器、环境传感器和大量的公共数据集。

在某些情况下,选择随机化的数据值是很有用的。考虑一下程序生成的游戏内容。与其手动构建关卡,不如编程使游戏自动生成地下城布局、地形、叙事元素和敌人生成位置。当然,这样的游戏应该包含合理的约束条件;例如,限制一次出现的敌人总数,并确保关卡布局不会是无法通行的。

游戏角色可以通过随机选择模块化组件构成,或完全通过形状和公式生成。例如,我写了一个 Processing Python 程序,生成图 3 中显示的随机微生物怪兽。这个代码——改编自 Lieven Menschaert 的 NodeBox 脚本Aquatics!——生成一个具有随机填充颜色、形状(由一种叫做超级公式的东西定义)并且至少有三只眼睛的生物。它有 70%的概率在生物的边缘生长出毛发,而这些毛发可以被随机方向的水流所摆动。

f00003

图 3:Lieven Menschaert 的 NodeBox 脚本Aquatics!的 Processing.py 改编版

无数酷炫的创意编程项目存在——从涂鸦和写诗的机器人,到进化模拟器,甚至还有一个程序,它通过卫星图像寻找类似字母的建筑或基础设施(The Aerial Bold Project,由 Benedikt Groß和 Joey Lee 发起,2016 年)。

也许这种创意编程听起来对你来说有些太艺术化了?Processing 对于你一直梦想构建的赛车模拟器也不太合适,更适合后台 Web 开发也不是它的强项。没关系,使用 Processing 的 Python 模式进行创意编程并不一定是阅读这本书的最终目标。可以把它看作是探索 Python、其他框架、创意编程应用以及编程的一般起点。

我在哪里可以找到帮助?

编程是有回报的,部分原因在于它具有挑战性。如果你发现自己在某件事上挣扎,不要紧张;这是正常的!只要稍加坚持,你很快就能掌握那些让你卡住的知识点。

在线资源

如果你进展不顺利,可以向在线社区求助。你可以在官方 Processing 论坛的discourse.processing.org/找到专门的 Processing.py 分类。你会经常发现有人已经遇到并解决了你所面临的挑战;如果没有,可以创建一个新话题。顺便提一下,本书的作者经常在这个友好和热情的网络角落里潜伏。

官方的 Python 模式参考文档可在https://py.processing.org/reference/上查看。每个条目都包括描述和简短的代码示例。在你使用 Processing 开发环境时,保持这个网页的打开非常有用。

源代码和解决方案

你将输入大量代码。这是好事,因为最好的学习方式就是通过实践。然而,有时你可能会打错字,或者不明白为什么代码不工作。在这种情况下,能够访问一个完整、可工作的文件版本将会很有帮助。你可以访问本书中的所有代码,以及每章挑战的解决方案,网址是github.com/tabreturn/processing.py-book/。你还可以在www.nostarch.com/Learn-Python-Visually/找到本书的任何更新。

本书内容是什么?

本书从基础知识开始,随着学习的深入,逐步过渡到更高级的主题。因此,每一章都需要掌握前一章介绍的概念。你将通过一系列实际任务,逐步完成学习目标。你还会找到一些理论内容,丰富的视觉效果,并通过挑战来巩固所学的知识。

以下大纲简要概述了每一章的内容:

  1. 第一章:你好,世界! 本章涵盖了本书的安装与设置程序,并介绍了如何用代码绘制基础图形。你还将学习计算机如何处理颜色,如何使用变量存储和重用值,以及如何使用 Python 执行基本的算术运算。

  2. 第二章:绘制更复杂的形状 在第一章中学习了一些绘图基础后,你将继续学习绘制更具有机形状的图形,而非几何图形。你将学习如何通过使用点(或顶点)和曲线来定义形状,这使得你可以用代码绘制几乎任何形状。

  3. 第三章:字符串简介与文本处理 本章将教你如何使用 Python 的字符串功能来处理文本。你还将学习如何使用 Processing 函数在显示窗口中绘制文本,使用不同的样式、颜色和字体。

  4. 第四章:条件语句 在这一章,你将开始像程序员一样思考。本章将引入控制流的概念,也就是说,你将学习如何编写能够做出决策的程序,执行不同的操作来应对不同的情况。

  5. 第五章:迭代与随机性 本章将教你如何编写可以重复某个操作特定次数,或者直到满足某个条件的程序。在本章末,你将尝试随机性并创建平铺图案。

  6. 第六章:运动与变换 本章主要讲解如何在 Processing 程序中添加运动效果,并对绘图空间进行变换。你还将学习如何将帧保存为图像,并如何从计算机获取时间值。你将运用这些技能创建一个动画屏保和模拟时钟。

  7. 第七章:使用列表和读取数据 Python 列表将为你提供强大的方法来管理和操作集合中的值。你将探索数据可视化的技巧。你还将学习如何从外部文件中读取列表数据。最后的任务是通过使用 CSV 文件渲染一个图表。

  8. 第八章:字典和 JSON 字典与列表类似,都是用来存储集合的项。然而,使用字典时,你通过使用一个键(通常是一个单词)来访问项,而不是通过引用项的位置。同样,你将运用新的字典技巧进行数据可视化。你还将学习如何处理 JSON 数据。

  9. 第九章:函数与周期运动 你将使用函数将程序划分为具有可重用代码的命名部分。这将使你的代码更加模块化,易于阅读和修改。你还将深入学习一些三角学知识,以生成椭圆形和波动类型的运动。

  10. 第十章:面向对象编程与 PVector 你可以使用面向对象编程,通过建模现实世界的物体来结构化程序。在这一章中,你将采用面向对象的方法,构建一个变形虫模拟。你还将学习如何使用 Processing 的 PVector 类来编程控制变形虫的运动。

  11. 第十一章:鼠标与键盘交互 在这一章中,你将为你的程序添加互动功能。Processing 可以处理来自各种设备的输入,但这里你将专注于鼠标和键盘输入,构建一个绘画应用。在这个过程中,你将了解事件函数,以及如何控制 Processing 的绘制循环行为。

出发吧!

你通过这些章节的进度,可能会受到之前在类似领域的经验影响。如果你之前做过任何类型的编程,无论是 Python 还是其他语言,你会遇到一些熟悉的概念。不过,这不是比赛!享受这个过程,适时休息,如果你感到很有灵感,也可以随时偏离路线。

第一章:Hello, World!

在学习一门新的编程语言时,传统的第一行代码通常是显示 “Hello, World!” 消息。延续这一传统,你在这里也会写出这样的代码——但这不仅仅是全部。本章将介绍你需要了解的 Processing 基础知识,之后你将快速从简单的 “Hello, World!” 过渡到使用代码绘图。

要开始使用,你需要为 Processing 设置 Python 模式,这样你就可以创建自己的草图。在此过程中,你将学习如何在 Processing 中编写代码的基本规则,以及如何处理错误、使用变量和执行算术运算。你还将了解 Processing 如何处理颜色,以及如何使用弧度测量角度。到本章结束时,你将能够通过使用各种 Processing 函数绘制色彩斑斓的几何图形。让我们开始吧。

Processing 安装与 Python 模式设置

在编写任何代码之前,你需要为 Processing 设置 Python 模式。前往 Processing 的下载页面(processing.org/download/)并下载适合你系统的 Processing 版本(Windows、Linux 或 macOS)。截至 2021 年 1 月,Processing 3.5.4 是最新的稳定版本。

Processing 不使用安装程序。相反,你只需解压你下载的文件(通常是 .zip 压缩包)并运行应用程序。具体过程在操作系统之间稍有不同:

  • 在 Windows 上,通过右键点击文件并选择 提取全部 来解压所有内容,然后按照指示操作。将文件夹提取或移动到计算机上的任何位置,包括 Program Files 文件夹或 Desktop

  • 在 macOS 上,双击文件进行解压,然后将提取出来的应用程序移到计算机上的任何位置,包括 Applications 文件夹或 Desktop

  • Processing 的 Linux 版本是一个 .tar 压缩包。将文件夹提取或移动到计算机上的任何位置,包括你的主文件夹或桌面。

完成后,打开新提取的文件夹。图 1-1 显示了你在文件管理器中能看到的简化列表。接下来,定位并运行名为 processing 的可执行文件。在 macOS 上,你将只有一个名为 processing 的文件。

f01001

图 1-1:Windows 或 Linux 的新提取的 processing 文件夹内容

应用程序的布局在不同系统和 Processing 版本中可能略有不同,但关键元素已在 图 1-2 中概述。如果你是 Mac 用户,你会在屏幕顶部找到 菜单栏。请注意,Processing 界面右上角的按钮标记为 Java。这是因为 Processing 默认包含了 Java 模式。

f01002

图 1-2:Processing 界面

接下来,激活 Python 模式。点击Java按钮,从下拉菜单中选择添加模式;然后,在弹出的贡献管理器窗口中选择Python 模式 for Processing。最后,点击安装。现在,你可以通过下拉菜单在 Python 和 Java 模式之间切换。切换到 Python 模式(见图 1-3)。

f01003

图 1-3:右侧的按钮表示已激活 Python 模式。

现在,你准备好编写你的第一行代码了!

你的第一个草图

Processing 将程序称为草图。考虑到你可能制作的内容的视觉和艺术性质,这个词非常合适。选择文件新建来创建一个新的草图,或者使用菜单旁边列出的快捷键。

输入以下代码:

size(500, 500)print('Hello, World!')

我稍后会详细讲解这段代码。现在,使用文件另存为保存草图,并将其命名为hello_world

你会注意到,Processing 创建了一个名为hello_world的新文件夹;其中有两个文件:hello_world.pydesketch.properties(见图 1-4)。根据你的系统配置,你可能看不到文件扩展名(.pyde)。要重新打开任何草图,只需找到并打开.pyde文件。

f01004

图 1-4:你的hello_world草图文件夹内容

你可能还想在草图文件夹中添加其他资源,如图片和字体,不过这些稍后再讲。

接下来,点击播放(▶)按钮执行代码。更好的方式是使用关联的快捷键:Windows 和 Linux 使用 ctrl-R,macOS 使用-R。此时,应该会出现一个灰色的 500 × 500 像素的显示窗口。在编辑器底部的黑色区域,即控制台中,Processing 应该显示Hello, World!(见图 1-5)。

现在让我们回到你在这个文件中输入的代码,它使用了两个 Processing 函数:size()print()函数是命名的指令,有点像是给计算机的狗命令。有些命令很简单,比如“坐下”,但像“取物”这样的命令可能需要指定 Fido 应该取回的具体物品。

Python 函数由函数名称和开闭括号组成,在括号内提供参数。在我使用的狗命令类比中,“球”可以是“取物”的一个参数。size()函数(见图 1-6)接受两个参数:第一个表示草图的宽度,第二个是高度。

在这种情况下,显示窗口的宽度为 500 像素,高度为 500 像素。

f01005

图 1-5:显示窗口(左)和控制台中显示′Hello, World!′的编辑器(右)

f01006

图 1-6:size()函数的结构

print()函数用于输出到控制台。此函数接受一个参数:字符串'Hello, World!'。因为这是文本数据——或者严格来说,是字符串数据——你需要将其放在引号中。你可以使用单引号或双引号,但请确保开闭引号类型一致。

Python 会根据数据类型对每个值进行分类,这决定了该值的处理方式以及你可以在其上执行的操作。例如,你可以对数值数据类型执行算术操作——如除法或减法——但不能对字符串执行。 在本章中,你将处理三种数据类型:

  1. 字符串 文本数据,如‘Hello, World!’

  2. 整数 没有小数点的数字,如 1、–27 或 422

  3. 浮点数 包含小数点的数字,如 1.618

Processing 代码与标准 Python 代码的不同之处在于它的一些函数;例如,size()函数是 Processing 特有的。换句话说,它在 Processing 环境之外无法使用。另一方面,print()函数是标准 Python 编程语言的内置元素。它在 Processing 的 Python 模式和任何其他 Python 程序中都能工作。

在本书中,我通常在Processing的上下文中提到 Processing 特有的功能,而将标准 Python 功能称为Python。如果这让你感到困惑,可以把它们看作是同一个东西。此时,区分 Processing 和 Python 并不关键;你会随着时间的推移理解其中的差异。

注释

如果你希望 Python 忽略代码中的某部分,可以将其注释掉。这个功能对于自己或其他编辑代码的人留下便于理解的英文注释非常有用。让我们在你的hello_world文件中添加几个注释:

1 # dimensions of the display window measured in pixelssize(500, 500)print('Hello, World!') # writes hello world to the console area2 '''This is a multiline comment.Any code between the opening and closing triple-quotes is ignored.'''print('How are you?')

注释分为两种类型:单行注释和多行注释。如图所示,单行注释使用#字符,1 多行注释使用'''(或""")2。

在完成本书任务的过程中,添加注释以提醒自己代码的工作原理。注释在调试代码时也非常有用。例如,如果你怀疑某些代码行导致程序失败,你可以通过注释掉这些行来暂时禁用它们。

空白字符

Python 以及扩展的 Processing Python 模式,对空白字符非常敏感。你需要小心插入空格字符或制表符的位置。例如,给size()这一行添加一些空格,然后运行草图:

# dimensions of the display window measured in pixels size(500, 500)print('Hello, World!')  # writes hello world to the console area. . .

当你运行草图时,消息栏会变为红色,Processing 会显示错误信息(图 1-7)。Python 依赖缩进来区分代码块。缩进的行会导致程序出错,因为 Python 没有遇到任何代码来定义size()函数的新代码块。

f01007

图 1-7:空白错误

通过删除你刚刚添加的空格字符来修正代码。

随着你在这些章节中的学习,你会逐渐理解什么时候以及在哪里使用缩进。但现在,注意代码中的空格和制表符字符,这些都会影响代码的缩进。

错误

空格问题并不是你会遇到的唯一错误类型。偶尔你可能会漏掉某个括号、逗号或引号,特别是在刚开始时。试着从你的size()函数中去掉闭括号,像这样:

# dimensions of the display window measured in pixelssize(500, 500print('Hello, World!')  # writes hello world to the console area. . .

现在运行代码并观察控制台输出。注意消息栏中的提示(图 1-8)。相当聪明吧?

f01008

图 1-8:消息栏中的警告(红色)提示了 Processing 遇到错误的可能原因。

这是一个语法错误的例子,而这并不是你第一次遇到这种错误。就像英语句子必须以大写字母开头并以句号结尾一样,Python 函数必须有一个开括号和一个闭括号。当你有多个参数时,需要用逗号将它们分隔开。这套规则被称为语法。如果你不遵守这些规则,Python 会感到困惑并报告错误。

错误信息并不总是如此清晰或准确,但它们可以为你提供一个线索,帮助你开始搜索 bug。有时,将错误信息复制并粘贴到搜索引擎中,能帮助你找到解决方案。

颜色

在 Processing 中,你可以用多种方式描述颜色。为了简化起见,我将采用十六进制值作为第一个示例。如果你熟悉像 Adobe Photoshop、Adobe Illustrator、Inkscape 或 GIMP 这样的图形软件,你可能已经在这些程序的颜色选择器中看到过十六进制值。

Processing 包含了自己的颜色选择器(图 1-9),你可以通过菜单栏选择工具颜色选择器来访问它。你可以使用这个颜色选择器来混合和采样颜色值。以#号开头的值是十六进制值;你可以使用复制按钮将其复制,然后粘贴到代码编辑器中。

f01009

图 1-9:Processing 颜色选择器

你的屏幕通过混合三种原色显示颜色——就像你在美术课上混合红、黄、蓝颜料一样。然而,你的屏幕使用的是红、绿、蓝三种原色。此外,由于光以加法的方式混合颜色,当所有三种原色以最大强度结合时,像素显示为白色。相反,完全没有颜色的像素显示为黑色。其他颜色则包含了不同量的红、绿、蓝。例如,亮红色的混合如下:

100% | 0% | 0%

十六进制颜色值由六个十六进制数字(012、……、9ABCDEF)组成,并可以分为三对,每一对对应一个原色。下面是亮红色的值:

#FF0000

FF 代表红色量;中间的 00 是绿色;最右边的 00 是蓝色。由于一些原因,FF 相当于 100%。另外,记住你正在混合光线,所以 #FFFFFF 是白色,#000000 是黑色。这里还有其他一些例子:

  1. 100% 蓝色 #0000FF

  2. 深绿色 #006600

  3. 深灰色 #505050

使用选择器进一步实验,观察当你选择不同颜色时,十六进制值是如何变化的。

fill() 函数设置用于填充形状的颜色。根据你使用的颜色系统,它最多接受四个参数。对于十六进制颜色,只需使用一个参数:以 # 开头的六位数值,放在引号中。

在你的 hello_world 草图底部添加以下代码:

. . .fill('#FF0000')

你现在已经将填充颜色设置为红色。为了看到效果,让我们绘制一个矩形。rect() 函数用于绘制矩形,它接受四个参数:

rect(`x_coordinate`, `y_coordinate`, `width`, `height`)

前两个参数指定矩形左上角的位置(图 1-10)。Processing 的 x 坐标从显示窗口的左边缘开始;y 坐标从顶部边缘开始。

f01010

图 1-10:Processing 的坐标系统

显示窗口的左上角坐标是 (0, 0),右下角是 (500, 500)。因此,要将矩形向下移动,增加 y 坐标值。在你的 hello_world 草图中添加一条新的矩形代码:

. . .fill('#FF0000')rect(100, 150, 200, 300)

运行草图,确认输出与 图 1-11 匹配。尝试修改 rect() 的参数,调整矩形的大小和位置。

f01011

图 1-11:rect(100, 150, 200, 300)

你现在应该熟悉了 Processing 的坐标系统。rect() 是许多绘图函数之一;稍后你将了解更多。

在这一部分,你还学会了使用十六进制值定义颜色,这些值描述了不同量的红、绿、蓝光。你现在可以使用像 Processing 自带的颜色选择器来混合和采样任何你需要的值。你会看到在 Processing 中还有其他定义颜色的系统,但在本书的大部分内容中,你将使用十六进制值。

填充和描边

当你编写一条 fill() 代码时,之后的每个形状都会使用你指定的颜色进行填充。直到 Processing 遇到下一条 fill() 代码,这个颜色才会改变。这样,Processing 就像是画画:你拿起画笔,蘸上颜料,然后你画的每一笔都会受到上次选择的画笔和颜色的影响。当你想以不同的风格或颜色画画时,你更换画笔或蘸取不同的颜料。如果你想完全禁用填充,可以使用 noFill()

在你的 hello_world 文件末尾添加以下代码,绘制一个较小的红色矩形、一个橙色正方形以及一个没有填充的正方形:

. . .# small red rectanglerect(10, 15, 20, 30)# orange squarefill('#FF9900')1 rect(50, 100, 150, 150)# fill-less squarenoFill()2 square(250, 100, 150)

对于一个正方形,你有两个选择:使用 rect() 1 函数,并传入匹配的宽度和高度(第三个和第四个)参数。或者,使用 square() 2 函数,它需要三个参数:x、y 和范围。

Processing 会从上到下解释代码行。因此,代码底部的形状会出现在视觉“堆栈”的顶部。所以前面的代码会生成图 1-12 中的形状。

f01012

图 1-12:没有填充的正方形——代码的最后一行是最上面的形状。

描边轮廓 的另一种说法,你可能会使用以下三种描边函数:stroke() 用于更改颜色,strokeWeight() 用于更改宽度,noStroke() 用于完全禁用描边。与 fill()noFill() 类似,描边函数会影响它们下面的所有内容。

要获得 3 像素宽的白色描边,可以在形状代码之前插入以下代码:

. . .stroke('#FFFFFF')strokeWeight(3)fill('#FF0000'). . .

stroke() 行会影响它之后的每个形状。图 1-13 显示了现在所有形状都有白色描边。

f01013

图 1-13:添加白色描边

对于更粗的描边,你可能需要指定角落和尖端是圆形的还是尖锐的。有关更多信息,请查阅相关的 Processing.py 参考条目,了解 strokeCap()strokeJoin()

背景颜色

要更改背景颜色,使用 background() 函数。在你的代码末尾添加一个背景行:

. . .square(250, 100, 150)background('#004477')

运行代码并注意到所有内容都消失了;整个显示窗口现在是一个平坦的蓝色。这是因为 background('#004477') 会覆盖它之前的所有内容,当你开始处理动画时,这会非常有用。现在,将该行移动到代码顶部,这样你可以再次看到形状 (图 1-14):

# dimension of the display window in units of pixelssize(500, 500)background('#004477'). . .

请注意,background 函数也可以接受图像作为参数(我将在第二章中介绍图像)。

f01014

图 1-14:添加背景颜色

颜色模式

我将在本章中使用十六进制颜色值,但这里先简单介绍其他颜色模式,因为在某些情况下,你可能需要使用非十六进制的颜色表示。例如,假设你想写一个代码来加深鲜红色的填充。首先,你会回忆起这是一种鲜红色:

fill('#FF0000')

你也可以将这个颜色表示为 RGB 格式:

fill(255, 0, 0)

在这种安排中,每个红/绿/蓝值是以逗号分隔的。正如你可能已经推断出来的,255 等于 FF(它本身等于 100%)。为了使红色变得一半亮度,你可以从 255 中减去 127。然而,试图从 FF 中减去 127 会有点复杂,因为你在处理十六进制和十进制数字的混合。在这种情况下,使用十进制值更容易(255 – 127 = 128)。

使用fill(255, 0, 0)时,colorMode()设置为RGB。不过,你不需要显式指定这个,因为它是默认模式。它的工作原理是:如果 Processing 检测到一个带引号的单一参数(例如'#FF0000'),它会将其解读为十六进制,但如果你提供三个参数,它会自动识别你正在使用 0 到 255 的系统。

然而,你可以使用另一种模式:HSB。一旦设置为HSB模式,三个fill()参数分别表示色相饱和度亮度。为了更好地理解这些变量如何影响颜色,我们来看一下 GIMP 这款开源图像编辑器的颜色选择器(图 1-15)。

旋转大三角形调整色相值 H 在 0 到 360 度之间;H(色相)字段对应三角形右下角的白色线条。你可以移动三角形内部的小白圆圈来调整 S(饱和度)和 V(亮度)字段。亮度在这个上下文中是可以互换的术语,因此 V 对应于 HSB 中的 B。

f01015

图 1-15:色相:330 度;饱和度:90%;亮度/值:80%

如果你安装了 GIMP,或者有类似的颜色选择器软件,我鼓励你进行实验。为了在 Processing 中模仿 GIMP 的配色方案,可以相应地设置颜色模式:

colorMode(HSB, 360, 100, 100)

HSB代表模式,360表示色相的度数范围,两个100参数表示饱和度和亮度的 0 到 100 百分比范围。现在你可以这样写一个红色填充:

fill(0, 100, 100)

这是因为鲜红色位于色相环的 0 度位置(从 GIMP 调色器中的“东”开始),其饱和度和亮度为 100%(图 1-16)。

f01016

图 1-16:色相:0 度;饱和度:100%;亮度/值:100%

在 HSB 模式下,在色谱上移动——从红色到橙色,再到黄色、绿色,依此类推——只需要对 H 值进行加减。尝试在 RGB 模式下做同样的事情就不那么容易,因为你需要调整每种原色的比例。

接下来的章节将进一步介绍颜色相关的内容。如果你需要更多细节,请参考colorMode()fill()的相关 Processing.py 参考文档。

2D 基本图形

接下来我们开始绘制基本形状。新建一个草图(文件新建)并将其保存为primitives_2d文件另存为)。在继续之前,添加以下代码来进行设置:

size(600, 300)background('#004477')noFill()stroke('#FFFFFF')strokeWeight(3)

现在,当你运行草图时,由于background('#004477'),会出现一个空白的蓝色显示窗口。你绘制的任何形状将没有填充,边框是 3 像素的白色。

接下来,通过point()函数绘制三个点(图 1-17):

point(100, 25)point(200, 25)point(150, 75)

point()函数接受两个参数,分别表示 x 和 y 坐标。当前的strokeWeight()决定了点的大小。

f01017

图 1-17:通过point()函数绘制的三个点

接下来是几个绘图函数的描述,以及要添加到你的工作草图中的代码。尝试调整参数,看看完成后的效果如何(见图 1-18)。

f01018

图 1-18:各种 2D 基本图形

triangle()

triangle()函数绘制一个三角形。六个参数表示三对 x-y 坐标。我将每一对 x-y 坐标通过去掉每两个参数之间的空格进行了分组,这样更容易阅读:

triangle(100,25, 200,25, 150,75)

Python 对参数之间的空白不敏感,所以如果你发现将代码格式化成这种方式对你有帮助,可以随意使用这种格式。

ellipse()

ellipse()函数绘制一个椭圆。第一个参数对表示椭圆的中心点坐标,第二对参数表示椭圆的宽度和高度:

ellipse(100,100, 100, 50)

对于圆形,你可以使用ellipse()函数,传入相等的宽度和高度(第三个和第四个)参数。或者,你可以使用circle()函数,它需要三个参数:x,y 和直径。

circle(100,100, 50)

circle()square()函数在 Processing 中是相对较新的,因此你可能会发现很多示例(文件示例)和在线代码仅使用ellipse()rect()

quad()

quad()函数绘制一个四边形(四边形)。本质上,它就像一个三角形函数,只是多了一个点,八个参数表示四对 x-y 坐标:

quad(260,180, 360,200, 380,250, 260,280)

line()

line()函数绘制一条直线,连接两个点。第一对参数表示起始的 x-y 坐标,第二对表示结束的 x-y 坐标:

line(450,80, 520,220)

和点和形状一样,线条的宽度会受到前面任何strokeWeight()函数的影响。

2D 基本图形函数提供了一种简便的方法来在显示窗口中绘制图形。还有一个图形函数需要复习,那就是arc(),但是它比其他图形函数稍微复杂一些。变量和算术运算符对于绘制弧形将非常有用,因此我将先介绍这些内容。不过,在继续之前,这里有一个小挑战,帮助你巩固所学的内容。

挑战任务#1:彩虹任务

开始一个新的草图(文件新建)并将其保存为rainbow文件另存为)。添加以下代码来开始:

size(600, 300)background('#004477')noStroke()

使用你迄今为止学到的内容,完成图 1-19 中的彩虹。

提示:考虑如何重叠图形来遮盖其他图形。如果需要帮助,可以访问解决方案:github.com/tabreturn/processing.py-book/tree/master/chapter-01-hello,world!/rainbow/

f01019

图 1-19:重新创建这个彩虹。

变量

变量是信息的占位符——就像你在代数中使用字母表示一个值一样。事实上,Python 变量的外观和行为非常相似。

开始一个新的草图并将其保存为 variables。为了简化操作,你将把值打印到控制台区域。添加以下代码来设置草图并打印其宽度和高度(以像素为单位):

size(600, 400)background('#004477')noStroke()print(width)print(height)

如果你运行草图,显示窗口的宽度和高度应该会被打印到控制台,如图 1-20 所示。

但请注意,你从未明确地定义 widthheight。Processing 会自动将显示窗口的宽度和高度赋值给这两个变量。由此可知,widthheight 是由 Processing 维护其值的变量。这些预定义的变量称为 系统变量

然而,你并不局限于使用系统变量;你也可以定义自己的变量。声明新变量时,通过使用等号 (=) 为其赋值,这个操作符被称为 赋值运算符。尝试用一个名为 x 的新变量来实现这一点:

. . .
x = 10print(x) # displays 10 in the console

f01020

图 1-20:将变量打印到控制台

变量 x 等于 10,因此 print() 函数在控制台显示 10

你可以随意命名你的变量,只要名称中只包含字母数字字符和下划线,不以数字开头,并且不与任何保留的关键字或变量(如 width)冲突。例如,以下展示了几个可能的变量名(注释标明哪些是正确的):

playerlives = 3 # correct
playerLives = 3 # correct
player_lives = 3 # correct
player lives = 3 # incorrect (contains a space)
player-lives = 3 # incorrect (contains a hyphen)
player2lives = 3 # correct
2playerlives = 3 # incorrect (begins with a number)

是否应该使用 camelCase、下划线或其他命名规则来命名多单词变量,是一种风格问题(也是争论不休的话题),但最好决定一个命名约定并坚持使用,因为你将在 Processing 中大量使用变量。

现在,向你的脚本中添加三个变量,用作 rect() 函数的参数:

. . .
y = 30
w = 100
h = wrect(x, y, w, h)

y 变量表示 y 坐标;w 表示宽度;h 表示 rect() 函数的高度值。请注意,h 值等于 w 值(即 100)。你已经将 x 定义为 10。运行草图确认它在显示窗口的左上角附近显示了一个白色的正方形(见图 1-21)。

f01021

图 1-21:使用变量作为坐标的正方形

自己进一步实验形状和变量。在下一节中,你将学习如何使用变量进行数学计算。

算术运算符

算术运算符操作数执行算术运算;这比听起来要简单得多。例如,在表达式 1 + 3 中,"+" 是运算符,而数字 1 和 3 是操作数。为了更好地理解 Python 中这一切是如何工作的,我们来看一些示例。

基本操作

在你的变量草图末尾添加以下一行,计算变量x加 2 的结果:

. . .print(x + 2)

运行草图并检查控制台输出的最后一行。我猜测代码会做你预期的事情。在本章的早些时候,你为变量x分配了一个值 10,而 10 + 2 等于 12,这就是你在控制台上看到的结果。

你还可以进行减法运算(请参阅注释以查看结果):

. . .print(x + 2) # displays 12print**(x - 2)** # displays 8

使用*运算符进行乘法:

. . .print(x * 2) # displays 20

现在试试这一行,但在运行之前,看看你是否能预测结果:

. . .print(1 + 2 * 3) # displays ???

控制台显示的是7而不是9,因为乘法先于加法发生。某些运算符的优先级高于其他运算符。还记得 PEMDAS 吗?它是一个助记符,帮助你记住运算顺序,即首先是括号,然后是指数运算,然后是乘法/除法,最后是加法/减法。(有些人可能更熟悉 BEDMAS 或 BODMAS,它们使用括号替代括号,以及oforder来表示指数运算。)

如果你想先进行加法运算,请使用括号:

print (1 + 2 * 3) # displays 7print((1 + 2) * 3) # displays 9

对于除法,使用正斜杠(/):

print(4 / 2) # displays 2

请注意,除以两个整数总是产生整数结果(整数是没有小数点的整数)。例如:

print(3 / 2) # displays 1

Processing 会丢弃任何小数位,实质上将结果向下取整。然而,需要注意的是,这种行为是 Python 2 的特性。在本文写作时,Processing 的 Python 模式使用的是 Python 2.7。如果你写的是 Python 3 代码,结果将是 1.5。

在 Python 2 中进行浮点数除法时,定义至少一个操作数时使用小数点:

print(3 / 2.0) # displays 1.5

这一行在 Python 2 和 3 版本中显示 1.5。 本书避免使用任何与 Python 3 不兼容的 Python 代码。请放心,你可以将你新获得的编码技能应用于 Python 2 和 3 的开发。如果 Processing 切换到 Python 3,你的代码仍然可以正常运行。

当然,除零操作将导致错误(图 1-22)。

f01022

图 1-22:除零错误

Processing 使用其他算术运算符(如地板除法和指数运算),这些在这里没有必要复习。然而,模运算符值得简单介绍。

模运算符

模运算符计算除法运算的余数,并用百分号符号(%)表示。以 5 除以 2 为例。你可以说答案是 2.5,或者你可以说答案是 2 余 1,因为 2“可以”除 5 两次,剩下 1。

模运算符执行后者操作并提供余数。下面是对比除法和取模运算的一些代码(如之前所示,注释中显示了输出结果):

print(5.0 / 2) # displays 2.5print(5.0 % 2) # displays 1.0

可能现在还不明显为什么这个运算符很有用。然而,许多重要的算法,如加密算法,使用模运算。暂时考虑一下,模运算结果为0表示数字整除得出精确的结果。除此之外,这对于判断一个数字是奇数还是偶数非常有用:

print(7 % 2) # displays 1, therefore 7 is oddprint(6 % 2) # displays 0, therefore 6 is even

在接下来的章节中,你将使用取模运算符。

现在我已经介绍了变量和一些基础数学内容,接下来可以介绍arc()函数,它用于绘制椭圆弧。让我们通过几个示例来看一下它是如何工作的。创建一个新的草图并保存为disk_space_analyzer。添加以下设置代码,定义一些视觉参数以便开始:

size(600, 600)background('#004477')stroke('#FFFFFF')strokeWeight(3)noFill()

arc()函数接受以下参数,这里将它们分多行显示以便更容易理解(请记住,Python 对函数参数之间的空白不敏感):

arc( `x_coordinate`, `y_coordinate`, `width`, `height`, `start_angle`, `end_angle`
)

通过使用start_angle0end_angle2,在草图中添加一个弧:

. . .arc( width/2, height/2,  200, 200,  0, 2
)

图 1-23 中的绿色叠加帮助说明了参数是如何工作的。Processing 会沿着一个不可见椭圆的边界绘制弧线,该椭圆的中心位于显示窗口的中间,x-y 坐标为width/2height/2;它的宽度为 200 像素,高度为 200 像素。角度0位于东面,顺时针打开到角度2,看起来大约是 115 度的旋转角度。

这个大角度的原因是 Processing 使用弧度而非度数来衡量角度;1 弧度大约等于 57.3 度。为什么要使用弧度呢?弧度作为一种标准的角度单位,在许多数学领域中使用,它为圆周运动提供了更自然和优雅的公式。想一想:为什么完整的圆是 360 度?为什么不是 300 度、100 度,甚至是百万度?顺便提一下,为什么一小时有 60 分钟?一天有 24 小时?这些都与古老的计数系统有关。

f01023

图 1-23:起始角度为 0,结束角度为 2 的弧

与将一个圆分割成任意数量的切片(比如 360 度)不同,弧度系统基于与圆的半径相关的比例度量。图 1-24 展示了弧度是如何定义的。从左边的图示开始,取任何圆的半径;创建一个相同长度的弧;然后测量弧的两端和圆心之间的角度,从而得出一个弧度。

f01024

图 1-24:定义弧度

如果 1 弧度大约等于 57.3 度,那么 2 弧度等于 114.6 度。这使得 180 度大致等于 3.142 弧度(图 1-25)。你认识这个数字吗?没错,它就是圆周率π!

f01025

图 1-25:测量半圆和圆周的弧度数量

Processing 提供了degrees()radians()函数,用于在两种单位之间进行转换,但如果你能记住一些关键的度量单位,使用弧度应该不会有问题。首先,0 度等于 0 弧度,180 度等于π弧度。因此,360 度等于 2π弧度。在 Processing 中,你可以使用系统变量PI来代替写出冗长的小数。

添加以下代码,使用arc()函数绘制半圆和完整圆:

. . .arc(width/2, height/2, 300, 300, 0, PI) # half-circlearc(width/2, height/2, 400, 400, 0, PI*2) # full-circle

运行草图。第一个新弧形从0开始,结束于PI,形成半圆;第二个最外层、最大的弧形结束角度为PI*2,因此它呈现为一个完整的圆。

如果你想关闭一个弧形,使其形成“切片”,可以添加一个额外的PIE参数。添加以下代码来测试:

. . .arc(width/2, height/2, 350, 350, 3.4, (PI*2)-(PI/2), PIE)

该弧形从 3.4 弧度(大约 10 点钟方向)延伸到约 4.7 弧度(12 点钟方向)。图 1-26 展示了最终结果。你可以通过其切片形状识别出最新的弧形。

f01026

图 1-26:这里有四个弧形,其中一个是完整的圆。切片状的弧形(左上)使用了PIE参数。

挑战 #2:磁盘使用分析器

接下来是进入第二章之前的最后一个挑战。磁盘使用分析器展示了磁盘驱动器内容的图形表示。Linux GNOME 磁盘使用分析器(也被称为Baobab)就是这种软件的一个例子,其图表很好地利用了弧形。

通过应用到目前为止学到的知识,重新创建图 1-27 中的环形图形。首先将现有的弧线注释掉,然后继续在同一个草图文件中工作。(文本和数字标签已经添加,以帮助你进行计算;在重新创建时不要添加它们。)

f01027

图 1-27:磁盘使用分析器图表

如果你需要帮助,记得可以访问github.com/tabreturn/processing.py-book/查看所有挑战的解决方案。

总结

现在你已经成功运行了 Python 模式的 Processing。你也学会了如何设置新的草图,设置显示窗口的大小,并应用背景颜色。你已经学会了在控制台中显示“Hello, World!”等消息,并使用 2D 基本函数绘制图形。你还学会了颜色的使用,并且知道如何使用十六进制、RGB 和 HSB 颜色模式来定义线条和填充的颜色。此外,你应该理解如何使用弧度来度量角度,并使用arc()函数。

在开始使用 Processing 的同时,你也学习了一些 Python 编程基础知识,比如如何管理空白字符、添加代码注释,以及使用算术运算符执行数学操作。你还了解了如何使用 Python 变量,它们是数据的占位符。Processing 包含了系统变量,比如widthheight,但你也可以将值存储在自己的变量中,只要变量名遵循 Python 的命名规则。

在第二章中,你将学习如何绘制更具有机感的形状,而不是几何形状。你还将深入了解像 Adobe Illustrator 和 Inkscape 这样的矢量图形软件的内部工作原理。

第二章:绘制更复杂的形状

在第一章中,你学习了 2D 基本图形,包括弧、椭圆、线条、点、四边形、矩形和三角形。然而,有些形状,比如心形、星形、八边形和皮卡丘轮廓,无法归入任何这种类别,需要使用更多的形状函数才能创建。

在本章中,你将学习如何使用点和曲线绘制更复杂的形状,以及使用顶点函数来布局点。通过这些技术,你将绘制出融合直线和曲线的形状,并通过从一个形状中减去另一个形状来创建形状。

你还将学习如何使用两种类型的曲线:Catmull-Rom 样条曲线Bézier 曲线。尽管这两者都涉及复杂的数学,但 Processing 的曲线函数处理了底层的微积分运算,使你只需控制点的坐标就能创建曲线。

显示网格

理解 Processing 中曲线如何工作的最佳方式是绘制几个曲线并对其进行操作。使用网格背景作为参考,可以更容易地绘制点和曲线,因此你将通过使用现成的图形来添加一个网格。创建一个新的草图并将其保存为curves,然后按照以下说明下载网格图形:

  1. 打开你的网页浏览器并访问github.com/tabreturn/processing.py-book/

  2. 导航到chapter-02-drawing_more_complicated_shapes

  3. 下载grid.png文件。

其他草图资源(图像、字体和其他媒体)应放在名为data的子文件夹中,因此在你的curves草图文件夹内创建一个新的data子文件夹,并将grid.png文件放入其中(图 2-1)。

f02001

图 2-1:将网格图形放置在你的data子文件夹中。

这个网格图形将位于你绘制的所有内容下方(图 2-2),帮助你衡量 x-y 坐标。使用以下代码设置你的草图:

size(500, 500)1 grid = loadImage('grid.png')2 image(grid, 0, 0)noFill()strokeWeight(3)

loadImage()函数加载图形文件并将其分配给名为grid的变量 1。image()函数 2 则将图像绘制到显示窗口。三个参数(grid00)分别表示加载的图像文件、x 坐标和 y 坐标。

图像将按其原始尺寸绘制,除非使用额外的第四(宽度)和第五(高度)image()函数参数来调整大小。

f02002

图 2-2:显示网格图像

使用 Catmull-Rom 样条曲线绘制曲线

要在 Processing 中绘制曲线,你可以使用curve()函数。此函数接受八个参数,它们表示四对 x-y 坐标;这些分别是起始控制点、曲线起点、曲线终点和终止控制点。

让我们从一条标准直线开始,然后将其转变为曲线。这样,你可以通过与更简单且熟悉的line()函数进行比较,直观地了解curve()函数是如何工作的。在你的curves草图中添加一条对角线(见图 2-3):

. . .stroke('#0099FF') # pale blueline(100,100, 400,400)

f02003

图 2-3:将直线转变为曲线

Processing 在指定的 x-y 坐标对之间绘制一条直线:(100, 100) 和 (400, 400)。注意,直线的坐标与下面的网格对应。

使用 curve() 绘制曲线

要使用curve()函数绘制相同的直线,请在curves草图中注释掉line()函数,并将其替换为curve()函数:

. . .stroke('#0099FF')  # pale blue#line(100,100, 400,400)curve(0,0, 100,100, 400,400, 500,500)

当你运行草图时,视觉效果应该与之前在图 2-3 中展示的完全相同。curve()函数括号中的四个中间值与line()函数相匹配,它们也表示曲线的起始和结束 x-y 坐标。

但是,curve()函数需要额外的四个外部参数(在这个示例中是0,0500,500),它们表示两对控制点坐标。这些控制点的位置决定了你对直线施加的曲率的方向和程度。在详细探讨之前,添加以下新行到代码的末尾,以绘制一条相同长度、相同位置,但带有曲率的黄色线条:

. . .stroke('#FFFF00') # yellowcurve(0,250, 100,100, 400,400, 500,250)

在这个例子中,四个中间参数保持不变,但控制点的坐标已改为0,250500,250。结果是一条带有轻微 S 弯的黄色曲线(见图 2-4)。通过比较蓝色和黄色线条,你可以直观地看到更改控制点如何影响曲线的形状。

f02004

图 2-4:黄色曲线的控制点,橙色圈出的部分,否则将不可见。

要理解控制点如何影响曲线,可以想象每个黄色曲线的端点延伸到相邻的控制点。你把控制点移近显示窗口的中心时,就相当于你在“弯曲”这条曲线。相反,如果将控制点 1 和 2 分别放置在显示窗口的左上角和左下角,四个点将排成一行,曲线就不再弯曲,从而形成一条直线。

为了观察控制点的作用,添加以下橙色曲线作为视觉辅助线:

. . .stroke('#FF9900') # orange# control point 1:1 curve(0,250, 0,250, 100,100, 400,400)# control point 2:2 curve(100,100, 400,400, 500,250, 500,250)

第一个curve()函数 1 从控制点 1 绘制一条橙色曲线,直到黄色曲线的起点;第二个curve()函数 2 从黄色曲线的终点绘制另一条橙色曲线,直到控制点 2。结果(见图 2-5)是一条三段式曲线(橙色-黄色-橙色),展示了控制点如何决定黄色部分的曲率。

f02005

图 2-5:你的 Processing 曲线(左)与传统样条(右)。(插图:Pearson Scott Foresman,公有领域许可。)

如你所见,橙色曲线延伸了黄色曲线,并说明了如果黄色曲线是一个完整的物理样条线,它会是什么样子。在图 2-5 的右侧,你可以看到没有计算机帮助下绘制此类曲线的柔性条带。如前所述,样条正是从这个条带得名。两个钉子对应curve()函数的起点和终点,而每端的 L 形件则代表控制点。

使用 curveTightness()更改曲线

curveTightness()函数决定了曲线如何严格地符合控制它的点,就像你用一条更具或更少柔韧性的材料代替了草图绘制的样条,或者将一条更短或更长的样条输入到相同区域中。该函数接受从-5.0 到 5.0 的值,默认值为 0。

要进行实验,在黄色描边上方添加一行curveTightness()

. . .curveTightness(0) # try values between -0.5 and 0.5stroke('#FFFF00')  # yellow

输入不同的值,以影响下方的曲线。图 2-6 展示了不同curveTightness()值下的曲线。

f02006

图 2-6:从左上角顺时针:curveTightness(-1)、curveTightness(0)、curveTightness(1),以及curveTightness(5)

图 2-6 中的右下曲线,紧度参数设置为1,因此曲线非常坚硬,结果是一条直的黄色线。你调整紧度值越远离1,曲线的变形程度就越大。对于超过起始点和结束点的曲线,可以使用大于 1 的值。例如,在紧度限制为5(左下)时,样条曲线会在通过起始和结束点时形成环路。而使用紧度参数-1(左上)时,较长的样条曲线会重新规划路线,以更好地与它所经过的点对齐,因此曲线更具弯曲性,但没有环路。

curve()函数直观且有用,用于快速生成曲线。但在 3D 建模、动画、计算机辅助设计(CAD)和矢量插图软件中,你最有可能遇到 Bézier 曲线,因此接下来我们将探讨这些曲线。

绘制 Bézier 曲线

Bézier 曲线 提供了一种直观且多功能的方式,通过一系列锚点和控制点来建模光滑的曲线。你可能在像 Adobe Illustrator 或 Inkscape 这样的矢量图形绘制软件中遇到过这些曲线。在本节中,你将使用bezier()函数绘制曲线。在图形软件中,你有可视化的节点可以抓取和操作;而在 Processing 中,你需要定义锚点和控制点的位置,使用bezier()函数的参数。

使用bezier()函数

bezier()函数有八个参数,这里将其拆分为多行,便于阅读:

bezier( `anchor_point_1_x`, `anchor_point_1_y`, `control_point_1_x`, `control_point_1_y`, `control_point_2_x`, `control_point_2_y`, `anchor_point_2_x`, `anchor_point_2_y`
)

第一对和最后一对参数是曲线的起点和终点。当使用贝塞尔曲线时,你通常将可见线连接的点称为锚点。从第一个锚点(anchor_point_1_x, anchor_point_1_y)开始,线条的曲率由其关联的控制点(control_point_1_x, control_point_1_y)的位置控制。另一个控制点(control_point_2_x, control_point_2_y)则控制线条朝向终点锚点的曲率(图 2-7)。不过,这种行为与样条曲线不同;相反,控制点更像是磁铁,导致线条向它们的方向膨胀。

要绘制贝塞尔曲线,创建四个变量来表示两个控制点的 x-y 坐标对:

. . .stroke('#FF99FF') # pink
cp1x = 250
cp1y = 250
cp2x = 250
cp2y = 250bezier(400,100, cp1x,cp1y, cp2x,cp2y, 100,400)

f02007

图 2-7:锚点和控制点控制贝塞尔曲线的位置和曲率。

第一对 bezier() 坐标将锚点 1 放置在网格的右上方;最后一对坐标将锚点 2 放置在左下方。所有控制点变量(cp1x, cp1y, cp2x, cp2y)都引用显示窗口的中心(250, 250)。通过将控制点沿锚点 1 和锚点 2 之间的对角路径放置,你将形成一条直线。接下来,你将这些控制点向外移动,以观察这如何使线条发生弯曲。

运行此草图以渲染一条粉色线,表示已拉直的贝塞尔曲线(图 2-8)。

f02008

图 2-8:粉色线条表示已拉直的贝塞尔曲线。

粉色线条应穿过显示窗口的中心(250, 250)与黄色曲线交汇。

锚点和控制点的定位

要将粉色线条调整为曲线(图 2-9),将 cp1x 变量设置为 200。除了这个变化之外,还需添加两行额外的代码:

. . .cp1x = **200**. . .bezier(400,100, cp1x,cp1y, cp2x,cp2y, 100,400)stroke('#FF0000') # redline(400,100, cp1x,cp1y)

f02009

图 2-9:通过调整控制点使粉色线条弯曲

额外的代码创建了一条红线,连接锚点 1(400, 100)和其控制点(cp1x, cp1y)。这条红线很有用,因为现在你可以可视化控制点的位置以及它控制哪个锚点。此外,在 bezier() 和红色 line() 函数之间共享变量意味着每次你调整定位曲线控制点(cp1x, cp1y)的值时,红线都会相应变化。将 cp1x 的值设置为 200 会使粉色线条产生弯曲,因为——随着控制点远离粉色线条——粉色线条向其膨胀。

曲线的上半部分受其顶部锚点连接的控制点(控制点 1)的影响最大;当你操作下方锚点的控制点时,这一点将更加明显。

现在添加另一条红线,将(下方的)锚点 2 和控制点 2 连接起来:

. . .cp2x = **320**cp2y = **420**. . .line(400,100, cp1x,cp1y)line(100,400, cp2x,cp2y)

新的红线将锚点 2(100,400)与其控制点(cp2x,cp2y)视觉连接。运行草图查看结果(图 2-10)。尝试不同的控制点值,观察它们如何影响曲线。

f02010

图 2-10:调整控制点 2

注意,粉色曲线的下半部分被“磁性”地拉向控制点 2。了解如何放置锚点和控制点,以便获得所需的曲线是需要技巧的。尝试下载并在 Inkscape(或如果你安装了 Illustrator)中练习。或者,试着在你的浏览器中玩 The Bézier Game,网址为bezier.method.ac/

现在,你可以使用 Catmull-Rom 样条曲线和 Bézier 曲线绘制曲线。curve()bezier()函数对于独立的曲线很有用,但如果要绘制由多个曲线段组成的形状,你需要使用顶点。

使用顶点绘制形状

在 Processing 中,顶点是用来连接线条以形成形状的点。顶点的复数形式是 vertices。你可以将 vertices 视为连线画图谜题中的点。例如,一个三角形需要 3 个顶点;一个五边形需要 5 个;而一个五角星()需要 10 个。当使用直线和曲线连接顶点时,形状的可能性变得无限。顶点不仅限于二维空间——例如,Blender 中的 Suzanne(一个猴头)有大约 500 个顶点,分布在三维空间中(图 2-13)。

f02013

图 2-13:约 500 个顶点中的三个被圈出(黄色)

你将通过一系列的vertex()函数绘制一个方形。创建一个新的草图并将其保存为vertices。在新的vertices文件夹中,添加一个data文件夹,并包含前一个草图中的grid.png文件的副本(图 2-14)。

f02014

图 2-14:vertices草图文件夹结构

添加代码以设置初始参数:

size(800, 800)
grid = loadImage('grid.png')image(grid, 0, 0)noFill()stroke('#FFFFFF')strokeWeight(3)

同样,你需要加载并显示网格图像,以帮助你在显示窗口中测量坐标。你绘制的每个形状将没有填充,且边框为 3 像素的白色。

现在,不再使用rect()square()函数,而是使用顶点来绘制一个方形:

beginShape() # begins recording vertices for a shape ...vertex(100, 100)vertex(200, 100)vertex(200, 200)vertex(100, 200)endShape() # stops recording

beginShape()endShape()函数对于将顶点分成不同形状至关重要。如果没有这两个函数,Processing 将假设草图中的所有顶点都属于同一个形状。也就是说,Processing 会忽略任何放置在beginShape()endShape()对外的“非法”vertex()线。如图 2-15 所示,代码绘制了一个没有左边的方形。

f02015

图 2-15:使用顶点绘制的开口方形

形状不会自动闭合,除非你包含一个endShape(CLOSE)参数,或者添加一个与起点连接的最终顶点。然而,活跃的fill()会填充颜色, 无论如何(图 2-16)。

f02016

图 2-16:尽管有一侧是开放的,形状仍然填充了颜色。

你还可以为beginShape()函数提供各种参数,以确定封闭顶点之间是否连接(如果有的话)(图 2-17)。

f02017

图 2-17:beginShape(POINTS)(左)和beginShape(LINES)(右)函数

对于仅由点组成的形状,使用beginShape(POINTS)。对于每隔一个顶点之间的线条,使用beginShape(LINES)。有关beginShape()参数的更多细节,请参考文档。

Bézier 顶点

bezierVertex()函数允许你在顶点之间绘制曲线。还可以使用curveVertex()函数来绘制 Catmull-Rom 类型的曲线,但本书侧重于 Bézier 类型,因为它提供了更大的控制力和更优美的曲线。

bezierVertex()函数有六个参数。为了理解这些参数的作用,你将继续完成图 2-18 中显示的其他形状。

我手动添加了淡蓝色的线条,其虚线的尖端提供了控制点的视觉指示。仅将这些线条作为参考使用;你不需要重新绘制它们。

f02018

图 2-18:一枚中国硬币(左下),S 曲线(中间),和心形(右)

S 曲线

S 曲线只是一个弯曲的线条,包含两个顶点,每个顶点都连接着自己的控制点。你将使用bezierVertex()函数来绘制它,以便将这个第一个示例保持尽可能简单,但通常,你会使用bezier()来绘制 S 曲线。

beginShape()endShape()之间,根据需要组合使用bezierVertex()vertex()函数。然而,首先的顶点总是通过vertex()来创建。开始一个新的形状并绘制第一个(在这种情况下是上方)顶点:

. . .# s-curvebeginShape()vertex(400, 200) # starting (upper) vertexendShape()

运行草图。由于没有第二个顶点来形成一条线,因此孤立的顶点应该显示为(400, 200)处的一个点。

现在通过使用bezierVertex()添加第二个顶点:

. . .# s-curvebeginShape()vertex(400,200) # starting (upper) vertexbezierVertex( 300, 300, # control point for the starting vertex 500, 500, # control point for the second (lower) vertex 400, 600 # second (lower) vertex coordinates
)endShape()

最后一对bezierVertex()参数(400, 600)表示第二个(下方)顶点的位置。第二个顶点连接到由第二对参数(500, 500)定位的控制点。第一对参数(300, 300)代表紧跟在bezierVertex()之前的vertex()函数的控制点。参考图像中给出的顶点位置(图 2-18),创建这个形状(图 2-19)实际上只是输入正确的坐标顺序而已。

f02019

图 2-19:完整的 S 曲线

这是一个开放形状,如果填充会显得不自然。接下来,你将研究一个闭合形状,但在继续之前,随时可以尝试不同的顶点和控制点值。

心形

你可以将心形想象成由两条弯曲的线连接到两个顶点。首先,绘制心形的一半(图 2-20):

. . .# heartbeginShape()vertex(600, 400)bezierVertex(420,300, 550,150, 600,250)endShape()

f02020

图 2-20:半个心形

你需要做的就是完成心形的右半部分。添加第二行bezierVertex(),看看能否填补缺失的参数:

. . .# heartbeginShape()vertex(600, 400)bezierVertex(420,300, 550,150, 600,250)bezierVertex(___,___, ___,___, 600,400)endShape()

回到图 2-18 查看控制点的位置。记住,你可以访问所有挑战的解决方案,网址为github.com/tabreturn/processing.py-book/

中国硬币

带有中间方孔的圆形金属硬币最早是在几百年前的中国出现的,但复制这种形状是学习 Processing 的一个好例子。要创建图 2-18 中的紫色硬币形状,你将使用beginContour()endContour()函数从圆形中减去一个正方形。

首先,你将使用beginShape()endShape()vertex()函数创建外形。然后,你将在beginShape()endShape()函数中放置beginContour()endContour()函数。在这对轮廓函数内,你将绘制一个第二个形状,它也由vertex()bezierVertex()函数组成;轮廓函数会从外形中减去这个形状。

第一个挑战是创建外圆。beginContour()endContour()函数无法从预定义的形状函数(如rect()ellipse()circle())中减去,因此你需要通过使用顶点来构造外圆。然而,使用贝塞尔曲线绘制圆形是可行的,你将通过创建一个菱形并使用控制点将其形成圆形。

从用vertex()函数形成一个菱形开始(如图 2-21 所示):

. . .# coinbeginShape()vertex(100, 600)vertex(200, 500)vertex(300, 600)vertex(200, 700)vertex(100, 600)endShape()

f02021

图 2-21:你将形成圆形的菱形

在顶点定位正确后,你可以继续为菱形添加曲线。当然,这将需要bezierVertex()函数,你将参考当前定位的顶点坐标。要了解如何定位额外的控制点坐标,请参见图 2-22。

f02022

图 2-22:定位顶点和控制点以形成圆形

图 2-22 显示了控制点应该如何定位,以形成最圆的形状。现在将每个vertex()替换为bezierVertex()函数。记住,第一点必须保留为vertex(),以形成圆形(图 2-23):

# coinbeginShape()vertex(100, 600)bezierVertex(100,545, 145,500, 200,500)bezierVertex(255,500, 300,545, 300,600)bezierVertex(300,655, 255,700, 200,700)bezierVertex(145,700, 100,655, 100,600)endShape()

f02023

图 2-23:使用bezierVertex``(``)函数形成的圆形

在圆形到位后,你可以开始从中间移除正方形。同样,通过使用顶点而不是预定义的形状函数(如rect()square())来定义这个正方形。这是一个相对简单的练习,但请注意,你需要为被减去的形状使用反向旋转:你必须将正方形的顶点按与放置外部形状(圆形)顶点相反的方向排列。

再次阅读圆形的代码,并注意顶点是按顺时针顺序绘制的;这意味着正方形的顶点必须按逆时针顺序绘制——也就是说,与其将要减去的形状的旋转方向相反。如果你没有正确设置这个方向,将不会发生减去操作。

将正方形的顶点放入beginContour()endContour()函数中。当然,除非你添加填充,否则你无法看到效果(如图 2-24 所示):

# coin1 fill('#6633FF')beginShape()vertex(100, 600)bezierVertex(100,545, 145,500, 200,500)bezierVertex(255,500, 300,545, 300,600)bezierVertex(300,655, 255,700, 200,700)bezierVertex(145,700, 100,655, 100,600)2 beginContour()vertex(180, 580)vertex(180, 620)vertex(220, 620)vertex(220, 580)3 endContour()endShape()

f02024

图 2-24:完成的硬币

如果没有填充 1,你只会看到白色的轮廓。beginContour()函数 2 开始记录组成负形状的顶点。不需要bezierVertex()函数,因为正方形没有曲线。顶点按逆时针顺序排列,从正方形的左上角(180, 580)开始,直接向下(到180, 620),然后继续绕行,直到endContour()停止记录 3。

使用矢量图形软件生成形状

你可以使用矢量图形绘图软件来绘制形状,然后引用顶点和控制点的位置来编写 Processing 代码。这就是我如何绘制出图 2-25 中显示的 Python 徽标的蓝色参考线。

f02025

图 2-25:追踪包含顶点和控制点位置的 Python 徽标。(Python 软件基金会的徽标商标政策可在www.python.org/psf/trademarks/查看。)

如果你喜欢挑战,可以清除你的曲线草图,尝试完成我在图 2-25 中开始绘制的 Python 徽标的半部分。这里有一些代码可以帮助你开始绘制轮廓:

beginShape()vertex(262, 238)vertex(262, 178)bezierVertex(262,40, 370,30, 500,30)bezierVertex(630,30, 730,40, 735,178)endShape()

你还可以将矢量图形导出为 SVG 文件,以便在 Processing 中使用loadShape()shape()函数,而不是使用loadImage()image()函数。但请注意:SVG 支持并不总是可靠的,你可能需要花一些时间调整你的 SVG 导出设置,以确保它们在 Processing 中正确显示。

总结

你现在已经学习了 Processing 的大部分基本绘图功能。通过使用网格图形作为坐标参考,你学会了绘制模拟物理样条曲线的曲线。此外,你还学会了绘制贝塞尔曲线——这是一种平滑、优雅的曲线,你可以通过锚点和控制点来控制它们。你还了解了如何通过一系列顶点来绘制形状。当你用直线和曲线连接顶点时,形状的可能性是无限的。你将在接下来的许多任务中,运用曲线、顶点和第一章中学到的技能。

在第三章中,你将开始探索 Processing 的文本功能。这包括将文本绘制到显示窗口、对其进行样式设置以及加载字体。你还将了解 Python 内置的字符串数据处理功能。在本书后续的章节中,你将使用文本功能为图表和图形界面元素添加标签,并为图像添加对话气泡。

第三章:字符串简介与文本操作

在第一章中,你创建了一个‘Hello, World!’字符串并将其打印到控制台,但 Python 能做的远不止打印字符串数据。在这一章,你将使用运算符、函数和方法来操作字符串。字符串是大多数编程语言中的基本数据类型,你将在几乎所有编写的程序中使用它们。如果你需要向用户传达信息、捕获文本框的输入、从网页获取数据或执行涉及文本的任何任务,你都会使用字符串。

在这一章,你还将学习如何使用 Processing 的文本函数在显示窗口中渲染任何字符串为文本。Processing 可以使用不同的字体,以不同的大小和位置,绘制各种颜色和样式的文本。你可能会用这些功能来用字母作画、标注图表、显示高分表,或构建一个交互式界面。

字符串

在探索 Processing 的文本渲染函数之前,你需要先了解字符串的基本知识。根据定义,字符串包含一个或多个字符的序列。例如,‘hello’ 是一个包含五个字符的字符串;它以 h 开头,以 o 结尾。你已经在第一章中简要接触过字符串数据类型,在那里你用它来定义十六进制颜色值并在控制台打印文本信息。

要创建一个新的 'hello' 字符串并将其赋值给名为 greeting 的变量,可以使用以下代码:

greeting = 'hello'

Python 识别 hello 为字符串,因为它被引号括起来。你可以使用单引号或双引号,但一定要确保使用与打开时相同类型的引号将其关闭。

在 Python 中,你可以用多种方式操作字符串。要将 hello 转换为 Hello!,你需要将第一个字符大写并在字符串末尾插入一个感叹号。Python 提供了许多内置功能来执行这些操作,我将在这一节中介绍一些最有用的功能。

你将学习如何组合字符串,以及如何查找、计数和提取特定的字符序列。这些功能大多数仅适用于字符串数据类型。例如,你不能将整数或浮动值转换为大写字母,因为它们是数字。如果你在数字的末尾加上一个 ! 字符,它就不再是一个数字;它变成了一个包含数字字符和感叹号的字符串。另一方面,你不能用数字去除一个字符串。

以下示例使用除法运算符将整数 6 除以 3,在控制台打印出 2。但尝试将 'hello' 除以 3 会导致错误:

print(6 / 3) # displays a 2print('hello' / 3) # displays an error

Python 不能将字符串与整数相除,因此你会得到一个 TypeError 错误信息。然而,某些数学运算符可以作用于字符串。例如,'hello' * 3 会返回 hellohellohello。在本章稍后,你将学习如何使用 + 运算符连接字符串。

在 Python 中创建字符串

让我们开始创建一些新的字符串变量,了解 Python 如何处理不同类型的引号,并解决创建字符串时可能遇到的一些问题。

开始一个新的草图并将其保存为 strings。添加以下代码:

greeting = 'Hello, World!'print(greeting)

当你运行草图时,print() 函数会将 Hello, World! 输出到控制台。

记住,Python 期望字符串以引号开始和结束,那么当字符串本身包含引号字符时会发生什么呢?添加另一个字符串变量,看看当你有未配对的引号时会发生什么:

. . .
whatsup = 'What's up?'

Python 会将这个字符串解释为 What,忽略撇号后面的所有内容。会留下些许悬空字符和一个未配对的引号(s up?')。运行草图并观察错误信息(图 3-1)。

f03001

图 3-1:由撇号引起的错误

为了修复这个问题,请使用双引号:

whatsup = "What's up?"

或者,你可以通过使用反斜杠来 转义 撇号字符:

whatsup = 'What\'s up?'

反斜杠表示 Python 应该将撇号视为普通字符,而不是语言语法的一部分。如果你现在打印 whatsup 变量,它会显示以下内容:

What's up?

注意,控制台输出中不会显示反斜杠。

反斜杠是转义字符,因此如果你需要在字符串中包含反斜杠,必须在前面加上另一个反斜杠。例如,print('\\') 会在控制台中显示一个单独的反斜杠。

你已经看到如何在用双引号限定的字符串中嵌套单引号。事实上,这种方法同样适用于反向操作。例如,添加一个新的 question 变量,使用单引号内嵌双引号:

greeting = 'Hello, World!'print(greeting)whatsup = "What's up?"
question = 'Is your name really "World"?'print(whatsup)print(question)

运行草图确认没有错误。控制台应显示三个 print 语句的内容。

使用连接和字符串格式化

+ 运算符执行整数和浮点数的算术加法,但你也可以使用 + 运算符将多个字符串连接成一个序列或链。连接 是编程术语,表示 将多个元素结合在一起,它对于许多任务非常有用,例如将单词连接成句子和段落。在你的草图中尝试这个示例:

. . .
all = greeting + whatsup + questionprint(all)

这应该在控制台中显示以下行:

Hello, World!What's up?Is your name really "World"?

注意,连接操作会精确地将字符串按其定义连接在一起,不会自动添加空格,因此你需要显式地插入所需的空格字符。

要修复前面的输出,请编辑 all 变量所在的行:

. . .
all = greeting + ' ' + whatsup + ' ' + questionprint(all)

控制台应显示以下内容:

Hello, World! What's up? Is your name really "World"?

现在,这行代码包括了在代码中指定的空格。

连接的另一种替代方法是字符串格式化,而 Python 为此提供了format()方法(我将在第 60 页的“字符串方法”部分进一步解释方法)。你需要理解的是,format()通过替换占位符符号为值来工作,而不是将它们按顺序连接起来。你会发现,连接操作符适用于简单任务,但在构建较长且更复杂的字符串时,它可能会显得笨拙。

这是使用format()构建的相同一行:

. . .
all = '{} {} {}'.format(greeting, whatsup, question)print(all)

在这种方法中,Python 会将每一对大括号({})替换为对应的变量——也就是说,第一个大括号替换为greeting,第二个替换为whatsup,第三个替换为question。这样你就不需要通过+ ' ' +来手动插入每个空格字符了。

如果format()的替代方法似乎并不简洁,可以考虑使用连接的这个示例:

firstname = 'World'
o2 = 21
hi = "Hi! I'm " + firstname + ". My atmosphere is " + str(o2) + "% oxygen."

如果你打印hi,你会得到Hi! I'm World. My atmosphere is 21% oxygen。使用连接时,你必须仔细放置空格字符,而且很难看出hi这一行在做什么。此外,你还必须将o2变量包装在str()函数中,以将其从数字转换为字符串;如果不这样做,Python 会尝试(并失败)将整数和字符串进行算术加法。

将其与使用format()方法构建的相同一行进行比较:

hi = "Hi! I'm {}. My atmosphere is {}% oxygen.".format(firstname, o2)

这能更好地展示你将获得的结果。format()方法还可以处理数字到字符串的转换。你可以根据当前任务选择最适合的方法。

处理字符串长度

len()函数返回字符串中的字符总数。你可以用它来检查字符串是否包含超过 1 个字符,或者验证字符串是否适合发布到推特(最多 280 个字符)。你还可以使用len()函数来查找列表(第七章)或字典(第八章)中的项目总数。

len()函数接受一个参数;你可以使用greeting变量来试试看:

print(len(greeting))

这应该显示 13,即greeting字符串中的字符总数。

到目前为止,你已经学会了如何定义新字符串并通过小的字符串构建字符串,但在 Python 中你还能做更多的事情。在接下来的章节中,你将学习如何通过使用切片符号和字符串方法来操作字符串。

字符串操作

让我们在你的工作草图中添加一些代码,这样你就可以尝试一些字符串操作方法。你将通过使用切片符号来提取部分字符串,转换大小写字符,并查找和计数特定字符序列的出现次数。你可以使用这些技术来自动化处理字符串数据的过程——例如,扫描数据中的关键词,分析字符串或缩短它们。你可以随意尝试不同的值和参数,看看它们的反应。

切片符号

Python 的切片表示法提供了一种简单而强大的方法来提取字符串中的字符。你可以使用切片表示法来提取单个字符或子字符串。子字符串是任何连续的字符序列,它构成更长字符串的一部分。例如,一个 URL 字符串可能以子字符串http://开始。

要在新的字符串变量url上实验切片表示法,将这个变量添加到你的字符串草图中:

url = 'http://www.nostarch.com'

你将通过使用一对方括号([])来指定要提取的字符的位置(索引)。为了简化,假设我们提取url字符串中的第一个字符。不过请注意,这个索引系统是从零开始的,这意味着字符的索引是从 0 而不是 1 开始的。请参见图 3-2 作为字符索引的参考。

f03002

图 3-2:字符串索引系统从 0 开始。

使用0(零)来提取第一个字符:

print(url[0]) # displays: h

控制台应该显示一个h

第二个字符的索引是1

print(url[1]) # displays: t

控制台应该显示一个t,即http中的第一个t

使用冒号(:)来指定字符的范围。你可以使用这个来提取 URL 字符串中的方案http)以及冒号斜杠斜杠(://),这个范围从索引0到索引7

print(url[0:7]) # displays: http://

:7位于0的右侧,用于提取直到第一个w之前的字符。但因为你的范围是从索引0开始的,你可以省略冒号前的0,得到相同的结果:

print(url[:7]) # displays: http://

冒号位于索引值(7)之前,这意味着 Python 必须从字符串的左侧/开始位置提取直到第七个字符的所有内容。

如果将冒号放在索引之后,Python 会返回从指定索引到字符串结尾的所有内容。你可以用这种方式提取冒号斜杠斜杠后的所有内容,也就是你通常在浏览器地址栏中输入的 URL 部分:

print(url[7:]) # displays: www.nostarch.com

现在,你应该能在控制台中看到www.nostarch.com。这是 URL 的子域www)、域名nostarch)和顶级域名com)的组合,它们由点字符分隔(图 3-3)。

f03003

图 3-3:URL 的组成部分

你可以通过字符串切片操作来提取 URL 的每个部分。假设顶级域名(com)总是三个字符,你可以通过使用索引-3后跟冒号来提取它:

print(url[-3:]) # displays: com

负值表示从字符串的末尾(右侧)开始计算索引位置,因此url[-3]只会提取字符c。你可以使用冒号来提取c及其后的所有字符。无论 URL 多长,这段代码总是会显示最后三个字符。相反,使用[:-3],将冒号放在-3的左边,表示提取直到倒数第三个字符(http://www.nostarch.)。

要提取域名(nostarch),提取索引11-4之间的子字符串:

print(url[11:-4]) # displays: nostarch

这将适应任何域名。例如,如果你将 url 值更改为 http://www.nostarchpress.com,Python 会打印出 nostarchpress。但这只有在方案是 http 且子域是 www 的情况下有效。你可以使用字符串方法,这些方法将适应任何长度的方案、子域和顶级域名。

这种符号切片字符串的方式有几种其他方法,但这些方法现在应该足够了。你还可以使用切片符号从列表和字典中提取项,因此你将在涉及这些数据类型的章节中再次遇到它。

字符串方法

字符串方法对字符串执行各种操作,如将字符在大写和小写之间转换,以及搜索和计数字符和子字符串。你将在草图中使用字符串方法来验证你的 URL 是否包含方案、子域、域名和顶级域名。这不是字符串方法的全面回顾,但它将让你熟悉它们的某些操作。任何一本合格的 Python 参考书都会涵盖其余部分。

方法与函数

Python 中的方法看起来和行为上都很像函数。你通过其名称调用函数——像 print()——它为你执行预定义的任务。方法也类似,但它们与特定的对象相关联,例如字符串方法是与字符串对象关联的。函数可能接受或不接受参数,具体取决于你使用的函数;方法也是如此。

作为示例,让我们将 len() 函数方法进行对比。没有 len() 方法,但我们假设有一个,以便专注于方法和函数之间语法上的差异。

回想一下,len() 函数返回任何字符串中的字符总数:

urllength = len(url)

len() 函数接受 url 参数,并返回其包含的字符串的长度。url 字符串的总长度是 23 个字符,因此变量 urllength 的值等于整数 23

方法以点号(.)开头,并附加到你想要影响的数据上。如果 len() 函数是一个方法,你应该这样写:

urllength = url.len()

接下来,你将使用 upper() 方法将字符串字符转换为大写。

upper() 和 lower() 方法

upper() 方法返回一个字符串版本,其中所有小写字母都被转换为大写。它不接受任何参数。以下是一个例子:

urlupper = url.upper()
print(urlupper) # HTTP://WWW.NOSTARCH.COM

upper() 方法是一个字符串方法,因此你必须将其附加到字符串上。语法可能与 format() 类似,后者是你之前用来将花括号替换为字符串中文本值的方法。在这个例子中,变量 urlupper 的值是 HTTP://WWW.NOSTARCH.COM。当你无法使用粗体或斜体时,这个方法可能有助于突出某些关键短语。lower() 方法是 upper() 的反向操作,它将所有大写字母转换为小写。

count() 方法

现在,让我们验证 url 字符串是否包含 www 子域名。count() 方法返回一个字符或字符序列在字符串中出现的总次数,并且它需要一个参数来指示你要计数的字符。例如,你可以使用 count() 方法验证 URL 中是否包含三个 w 字符:

print(url.count('w')) # 3

你的控制台应该确认有三个 w 字符。但它并没有指示这些字符是否是连续的;这些字母可能分散在字符串中。为了更明确,使用参数 'www'

print(url.count('www')) # 1

子字符串 www 在这个字符串中只出现一次。但你不能确定这就是子域名。如果 URL 的域名部分包含 www 呢?你可以更具体一些,统计 http://www 出现的次数,但 HTTP 并不是唯一的 Web 地址方案。例如,HTTPS,HTTP 的安全扩展,用于加密计算机网络中的通信。更复杂的是,子域名可能不是 www

find() 方法

让我们尝试另一种方法。find() 方法返回任何字符或子字符串的索引。注意冒号斜杠斜杠 (://) 如何分割方案和子域名。使用 find() 方法来检索冒号斜杠斜杠的索引。添加代码来找到该索引,存储在一个名为 css 的变量中,然后用它来提取方案:

css = url.find('://') # 4
scheme = url[:css] # http

find() 方法检索 url 字符串中第一个出现的 :// 的索引。更具体地说,这是子字符串中第一个字符,即冒号的索引。如果找不到该子字符串,结果将是 -1。在这个实例中,索引为 4。请注意,这个参数区分大小写。

子域名位于冒号斜杠斜杠和第一个点之间。使用 find() 方法找到第一个点的索引,并使用切片符号提取并将子域名赋值给一个名为 subdomain 的变量:

dot1 = url.find('.') # 10
subdomain = url[css+3:dot1] # www

css+3 等于 7,即 www 中第一个 w 的索引。我加上了 3 来将起始索引偏移到冒号斜杠斜杠的长度。这对于 www 或任何其他子域名都有效(尽管如果没有子域名,你会遇到一些问题)。

顶级域名 (com) 从第二个点开始一直到字符串的末尾。如果某个字符或子字符串出现多次—例如点—你可以提供第二个 find() 参数来指示搜索应该从哪个索引位置开始。你可以使用 dot1 变量来表示这个偏移量,但你需要加上 1 来从它之后的字符开始。将顶级域名赋值给一个名为 tld 的变量:

dot2 = url.find('.', dot1+1) # 19
tld = url[dot2 + 1:] # com

dot2 变量等于 19,即 URL 中第二个点的索引。在 tld 这一行中,我将 19 作为起始索引参数加上了 1,因为我不想包含 .com 中的点。

find() 方法可以接受一个额外的第三个参数,用于指示搜索应该在字符串的哪个位置终止。

最后,将域名(nostarch)赋值给一个名为domain的变量:

domain = url[dot1+1:dot2] # nostarch

域名子串位于第一个和第二个点之间,但要加上1dot1,以避免获取第一个点字符。

你现在已经使用切片符号将 URL 分割成了各个部分。将切片符号与字符串方法结合使用,可以提供一种更强大的方式来完成此操作,这样你的程序就能处理不同长度的协议、子域和顶级域。

在下一部分中,你将学习如何使用 Processing 文本函数将字符串作为文本显示在显示窗口中,这样你就不再局限于在控制台中打印字符串。你可以使用文本作为装饰,标注视觉输出中的元素,或为用户提供反馈。

排版

排版指的是文本(或字体)的排列和样式,以使其更易读且更具美感。排版处理可以真正决定设计的成败。例如,标题最好能与其他文本区分开;字母间距应比单词间距更紧凑,而且你可能同意,草书字体不适合用于路标。虽然我不建议你用 Processing 排版一本书,但它确实提供了有用的功能来控制文本的外观。

字体

字体由许多字形组成;字形是任何单个字符,比如Aa?。如果你没有指定 Processing 应该使用哪个字体来绘制文本,它会依赖预定义的默认字体。你的计算机包括一系列预安装的字体,但不同操作系统之间的选择可能不同。你还可以在系统中安装额外的字体来扩展选择。不过,如果你在不同计算机之间移动或共享草图(它们的字体库不同),你可能会遇到问题。如果一个草图需要特定字体,而该字体未安装,Processing 就无法加载它。为避免这些问题,我将解释如何将字体文件与草图一起打包。

由于早期的计算机字体是基于像素的,它们需要为每个字体大小提供一组独立的字形。例如,如果一种字体有三种大小和一种斜体变体,它包含六个完整的字符图形集。然而,现代字体是基于矢量的,这就是为什么你可以将文本缩放到任何大小而不会出现像素化的原因。你不再需要为每个字体大小准备一个文件,但粗体和斜体变体仍然是独立的字体文件。

默认情况下,Processing 会使用标准的无衬线字体在显示窗口中渲染文本。在字体术语中,衬线是附着在字符顶端的小线条(见图 3-4 中的圆圈)。的意思是“没有”;因此,无衬线字体没有衬线。

f03004

图 3-4:字体分类

等宽字体也可以是衬线字体,但其特点是每个字符占用相同的水平空间。比例间距字体(如图 3-4 中的衬线和无衬线示例)通过使用内建的度量标准来指定每个字符与其相邻字符之间的间距,从而使文本更易读。例如,当im字符占用相同大小的“容器”时,会出现不自然的间距问题,这也是许多等宽字体尝试通过给i加大衬线并压缩m来解决的问题(见图 3-5)。这也意味着等宽字符在多行文本中会垂直对齐。

f03005

图 3-5:等宽字符具有固定的宽度

也就是说,等宽字体在某些情况下更易读。例如,当你需要让字符在列中对齐时,等宽字体会很有用:

Sam  Jan  Amy  Tim | Total
99   359  11   3   | 472

这种特性使得等宽字体更适合编写代码,这也是为什么 Processing 编辑器(以及其他所有代码编辑器)的默认字体是等宽字体。

文本函数

让我们创建一个新草图来实验 Processing 的文本函数。你将使用这些函数在显示窗口中绘制文本,并设置你的字体、字体大小、行间距和文本对齐方式。

启动一个新草图,并将其保存为typography。添加以下代码开始:

size(500, 320)background('#004477')fill('#FFFFFF')stroke('#0099FF')strokeWeight(3)

这段代码将背景设置为蓝色,填充颜色设置为白色。正如你很快会看到的,fill()颜色将影响你绘制的文本。任何描边都是淡蓝色的,宽度为 3 像素。

全字母句是一个包含给定字母表中每个字母至少一次的句子。创建一个名为pangram的变量,存储一个完美的英语全字母句:

pangram = 'Quartz jock vends BMW glyph fix'

从这里开始,你将渲染存储在pangram中的不同版本的字符串,如图 3-6 所示。

要重现图 3-6,从text()函数开始,text()函数将文本绘制到显示窗口中,其字体颜色由当前的填充颜色决定:

text(pangram, 0, 50)

f03006

图 3-6:你将渲染这些版本的相同全字母句。

运行草图。你应该能在显示窗口中看到全字母句的第一个(顶部)版本。参数(pangram, 0, 50)分别代表字符串值、x 坐标和 y 坐标。你可以添加额外的第三个和第四个参数来指定文本区域的宽度和高度,这些将在稍后使用。

textSize()函数设置所有后续text()函数的字体大小(以像素为单位)。添加以下代码以显示全字母句的第二个版本:

textSize(20)text(pangram, 0, 100)

运行草图以确认你有较小和较大的两种版本的全字母句。

请注意,垂直的淡蓝色线条(图 3-6)精确标记了最长/最大行文本的结束。添加这条线的目的是为了探索 textWidth() 函数,您可以使用它来计算显示文本的宽度。在这种情况下,你想要测量第二个完整句子的宽度并在其结束处绘制一条垂直线。使用 textWidth() 函数作为线条函数的参数:

line( textWidth(pangram), 0, textWidth(pangram), height
)

现在,单词的宽度作为行的起始和结束 x 坐标;起始和结束的 y 坐标分别是显示窗口的顶部和底部边缘。这将绘制一个淡蓝色的垂直规则线,表示第二个完整句子的结束,且高度与显示窗口相同。

你将用衬线字体渲染第三个完整句子。要切换到另一种字体,你需要知道字体名称并进行引用。要列出你计算机上安装的字体,可以使用 PFont.list()

print(PFont.list())

滚动浏览控制台输出,查看是否能找到CambriaGeorgia。这两种都是衬线字体。如果系统中未安装 Cambria 或 Georgia,你将无法在列表中找到它们。在这种情况下,任何其他衬线字体都可以使用,比如 Times New Roman。

Processing 使用它自己的字体格式,因此在使用之前,你需要使用 createFont() 函数转换字体。

添加一行 createFont(),其中包含一个字符串参数,表示你将使用的衬线字体的名称:

seriffont = createFont('Cambria', 20)

createFont() 函数接受两个参数:一个字体名称(如控制台列出的方式)和字体大小。前一行将转换后的字体分配给一个名为 seriffont 的变量,接下来你将使用这个变量。

要激活新的字体,使用 textFont()。然后,再次绘制完整句子(第三个版本),以确认它已生效:

textFont(seriffont)text(pangram, 0, 150)

textFont() 函数接受一个参数,即一个已准备好的字体。所有后续的 text() 函数都会使用这个 seriffont,直到 Processing 遇到另一个 textFont() 函数。

textLeading() 函数控制文本的行距。Leading(发音类似于wedding)是排版术语,用来描述每行文本之间的间距。

textAlign() 函数控制文本的对齐方式;你可以使用 LEFTCENTERRIGHT 作为参数来设置文本的水平对齐。

你将使用 textLeading()textAlign() 函数来渲染完整句子的底部两个版本(第四和第五个版本),如图 3-6 所示。添加左对齐和右对齐的完整句子:

1 textLeading(10)text(pangram, 0, 200, 250, 100)textAlign(RIGHT)text(pangram, 0, 250, 250, 100)

第一个全字句是左对齐的,因为这是 Processing 的默认设置。我为text()函数添加了宽度和高度参数,以启用自动换行。每个全字句都被限制在自己的矩形区域内,宽度为 250 像素,高度为 100 像素。如果一行文字超出了 250 像素的宽度,Processing 会自动将无法适应的单词推到新的一行。如果有任何行部分超过了文本区域的高度(100 像素),它们是不可见的,尽管在这里没有发生这种情况。行间距(leading)被减小到 10 像素 1,导致行与行之间重叠。通常情况下,行间距的值与字体大小成比例。

就像fillstroke和许多其他 Processing 属性一样,你设置的文本参数会一直有效,直到你明确指定其它值。但如果你调整文本大小——通过另一个textSize()函数——行间距将重置为一个成比例的值。

总结

这段简短的介绍探讨了通过使用 Python 的切片符号和字符串方法来操作字符串,并使用 Processing 的text()函数在显示窗口中绘制文本。Processing 的排版功能让你能够控制字体大小、水平对齐、行间距/行距以及字体选择。在接下来的许多任务中,你将使用字符串方法和文本函数。

在第四章中,你将探索包括控制流和条件语句在内的主题——这些技术允许你编写可以跳过、跳转或重复执行代码行的程序。这些工具很有用,因为它们让你能够根据特定的规则和值改变代码执行的顺序,甚至决定是否执行代码。

第四章:条件语句

到目前为止,你编写的程序是逐行执行的,从代码的顶部开始,一直到底部。你可以将这个执行流程想象为一系列按顺序执行的步骤,这意味着程序只能以一种方式运行。在本章中,你将学习如何编写不同的路径,让 Python 根据是否满足某些条件来选择不同的执行路线。这非常有用,因为你可以根据不同的场景执行程序中的不同操作——就像视频游戏根据你的表现将你引导到不同的关卡或屏幕。

要评估一个条件,你将使用布尔数据类型,它表示两种状态之一:真或假。你将学习编写布尔表达式来测试某个语句是否为真或假。然后,你会使用ifelifelse语句,根据真假结果让代码执行不同的操作。

控制流

控制流指的是代码行执行的顺序。默认情况下,这个流从代码的顶部开始,并按顺序逐行执行,直到达到代码的底部。通过使用控制流语句,如ifelifelsewhilefor,你可以指示 Python 跳过、跳转或重复某些代码行。

举个例子,假设你想用圆形填充显示窗口。图 4-1 展示了两种排列方式:9 个圆形按三行三列排列,81 个圆形按九行九列排列。

f04001

图 4-1:9 个圆形(左)和 81 个圆形(右)排列方式

你可以为每个显示的圆形编写一个circle()函数。如果你只绘制 9 个圆形,编写 9 个circle()函数可能是可行的,但如果要编写 81 个circle()函数,这将是繁琐的,并且容易出错。如果你需要多个圆形,更好的方法是编写一行circle(),然后让 Python 根据需要重复执行这行代码。图 4-2 展示了这两种方法,使用流程图表示编程逻辑。

f04002

图 4-2:手动方法(左)与条件方法(右)绘制多个圆形的流程图比较

手动方法如左侧所示。每个绘制圆形表示一个circle()函数;在这种情况下,有两个绘制圆形步骤,但你可以根据需要添加更多。

在图 4-2 中的右侧流程图会重复绘制圆形步骤,直到满足某个条件。包含81?的菱形表示一个决策步骤,检查当前圆形的数量是否为 81。如果为真,程序将继续执行停止步骤;如果为假,Processing 会绘制另一个圆形并返回到决策步骤。

本章及下一章将探讨如何在 Python 中实现这种逻辑,这将是你首次接触算法思维。之后的章节中,你将在大多数草图中应用流程控制技术。

条件语句

条件语句用于测试一个或多个条件,并根据测试结果执行相应的响应。

要探索 Python 的各种条件语句,创建一个新草图并将其保存为conditional_statements。在接下来的章节中,你将向这个工作草图中输入代码。

布尔数据类型

如前所述,布尔值是可以表示两种可能状态的值:TrueFalse。为了查看布尔数据类型的操作方式,请在草图中添加以下两个变量:

ball_is_red = True
ball_is_spiky = False

布尔值的首字母总是大写,并且不使用引号,因为那样会使其变成字符串。

每当 Python 需要将布尔值作为数值处理时,它会将True转换为1,将False转换为0;不过,这个过程是双向的。例如,Python 的bool()函数会将任何值转换为布尔值,将1转换为True,将0转换为False。当你遇到if语句时,这个特性非常有用,因你可以指示 Python 根据True/False的结果执行不同的代码行。

在你的草图中,添加一系列的print()函数来测试这种行为:

. . .print(ball_is_red) # displays: Trueprint(ball_is_spiky) # displays: Falseprint(ball_is_red + True) # displays: 2print(bool(1)) # displays: Trueprint(bool(0)) # displays: False

前两条print语句会将变量值重复输出到控制台。第三条print语句使用算术加法(+)运算符将一个True布尔值与另一个True布尔值相加。将TrueTrue相加的结果是2。将布尔值转换为数字可以与数学运算符一起使用,或者使用任何将值转换为数字的函数,例如int()函数,它将值转换为整数。最后两条print语句,其中包含bool()函数,将10转换为各自的布尔值。

关系运算符

前面的例子明确地定义了球是否是红色和/或有刺,但关系运算符也可以指引你的程序自行判断哪些是对的,哪些是错的。关系运算符,例如大于符号(>)和小于符号(<),用来确定两个操作数之间的关系。例如,给定3 > 2,3 和 2 是操作数,而大于符号是关系运算符。因为 3 确实大于 2,所以这个语句是正确的。

为了看到这个如何工作,向你的conditional_statements草图中添加以下代码:

. . .x = 2print(x > 1) # displays: Trueprint(x < 1) # displays: False

变量x等于2,大于1,所以控制台应该显示True。然而,2并不小于1,因此最后一行应该打印False。注意,关系运算符返回的是布尔值。这在下一部分中非常重要,因为这些比较结果将决定程序执行哪些代码行。表格 4-1 列出了 Python 的关系运算符。

表格 4-1:关系运算符

运算符 描述 示例
> 左操作数大于右操作数 2 > 1 返回 True
< 左操作数小于右操作数 1 < 2 返回 True
>= 左操作数大于或等于右操作数 1 >= 2 返回 False
<= 左操作数小于或等于右操作数 2 <= 2 返回 True
== 左操作数等于右操作数 2 == 2 返回 True
!= 左操作数不等于右操作数 2 != 2 返回 False

表 4-1 没有显示的是,==!=运算符可以同时作用于数字和字符串。添加以下代码来测试这一点:

. . .
name = 'Jo'print(name == 'Jo') # displays: Trueprint(name != 'Em') # displays: True

接下来,你将把关系运算符与if语句以及其他条件语句结合使用,以指定执行代码行的条件。

if语句

if语句需要两个元素:一个返回TrueFalse的表达式,以及在前者为True时要执行的代码。图 4-3 展示了if语句的语法。

f04003

图 4-3:if语句的语法

所有淡蓝色部分是占位符伪代码,它只是用英文描述代码中正在发生的事情;意思是,你以后可以用 Python 代码替换它。

分配及格成绩

要开始使用if语句,你将编写一个简单的程序,根据学生的考试成绩为他们分配字母成绩。首先,将以下代码添加到你的工作草图中:

. . .score = 60if score >= 50: print('PASS')

这段代码为任何分数大于或等于 50 的学生赋予PASS(通过)成绩。在这个例子中,score >= 50返回True,因此print('PASS')这一行会被执行。确保缩进print这一行,你可以通过按 Tab 键来完成。

任何在if语句下面缩进的内容都会在条件为True时执行。例如,将以下代码行添加到你的代码中:

. . .if score >= 50: print('PASS') print('Well done!')

现在,对于任何分数大于或等于 50 的学生,这段代码应该同时打印PASSWell done!

另一方面,靠左边的print语句无论分数是否超过 50 都会打印Well done!

. . .score = **10**if score >= 50: print('PASS')print('Well done!')

如果你需要在一个if语句中嵌套另一个if语句,请相应地增加缩进。大多数代码编辑器允许你选择多行代码并按 Tab 键进行缩进,Processing 的编辑器也不例外。如果你需要取消缩进,按住 Shift 键并同时按 Tab 键。

在没有添加下一个示例到你的代码中之前,看看你能否预测结果:

score = **60**1 language = 'ES' # for Español (Spanish)2 if score >= 50: 3 print('PASS') 4 if language == 'EN': print('Well done!') 5 if language == 'ES': print('Bien hecho!')

如果你预测控制台会先显示 PASS 行,然后显示 Bien hecho!,那你是对的。'ES' 字符串值被分配给一个名为 language 的新变量 1。分数大于或等于 50,因此程序会执行最外层 if 语句的内容 2。PASS 行 3 会首先打印。然而,下一条 if 语句 4 的条件评估为 False,因此程序跳过了 Well done! 行。最后的 if 语句 5 会检查西班牙语。由于语言变量等于 'ES',Processing 会将 Bien hecho! 打印到控制台。

分配字母成绩

目前,你的评分程序只能授予 PASS。要分配字母成绩,如 A、B 或 C,你需要使用更多的 if 语句。

修改你的代码,将 PASS 字符串改为 C,并插入一个新的 if 语句,对于任何大于或等于 65 的分数,授予 B:

. . .score = **60**if score >= 65: print('B')if score >= 50: print('C')

运行草图。由于 score 变量的值大于 50,控制台显示了 C。但是有一个问题——当你将分数改为 70 时,控制台会显示 B C(图 4-4)。

f04004

图 4-4:70 分获得了 B C。

因为分数大于 65 且大于 50,两个 if 语句都会被触发,导致打印出两个字母成绩。为了避免出现多个成绩,需要将 if 语句连接起来,这样如果第一个条件为真,后续的 if 语句会被跳过。这就是 else-if 结构发挥作用的地方。

elif 语句

else-if 语句,或 Python 中的 elif,只有在 if 条件返回 False 后才会运行。使用 elif 将解决之前的问题,即让两个 if 语句独立操作。因此,只需将第二个 if 改为 elif

. . .score = **60**if score >= 65: print('B')**elif** score >= 50: print('C')

现在,如果 score 的值是 B,就不需要检查 C 条件,Python 会完全跳过 elif 语句。另一方面,如果最初的 if 语句条件返回 False,那么 elif 会检查 score 是否大于或等于 50;如果是,它会打印出 C

score 变量设置为一个适合 B 的值,比如 70,然后运行草图。此时控制台应该显示 B,但没有 C

顺序很重要

正确排序 if...elif 逻辑非常重要。比如,考虑下面这段代码,将 C 条件放在了前面:

score = 70if score >= 50: print('C')elif score >= 65: print('B')

在这种情况下,任何大于或等于 50 的分数都会获得 C,即使它高于 65 应该得到 B。事实上,B 成绩永远不会打印到控制台,因为程序永远无法检查 B 条件。

检查 A

在你的 conditional_statements 草图中,插入一个新的 if 语句来处理 A 成绩(80 或更高)。另外,将 B 语句改为 elif。调整 score 来测试是否正常工作:

score = **87**if score >= 80: print('A')**elif** score >= 65: print('B')elif score >= 50: print('C')

你可以根据需要添加尽可能多的elif语句,但始终是一个if语句标志着if...elif链的开始。

现在你的 A/B/C 逻辑已经到位,但如果成绩低于 50,它将通过所有的if...elif语句而不触发任何动作,根本没有得到任何成绩。

else 语句

如果学生没有得到 A、B 或 C,可以得出成绩为FAIL的结论。为了处理FAIL情况,可以使用else 语句来处理任何不符合if...elif分组的条件。你无需检查score变量是否小于 50,因为这一点已经通过其未能匹配之前的任何条件而隐含了。为了处理FAIL情况,可以在代码中添加以下else语句:

. . . print('C')else: print('FAIL')

else语句没有条件,总是出现在if...elif分组的末尾。

score值调整为类似 40 的数值并测试代码。控制台应该显示FAIL

没有elifelse语句

else语句不一定需要紧跟在elif之后。你可以使用if...else结构,在不需要elif分支的情况下。例如,考虑一个程序,它将所有成绩为 50 及以上的视为PASS,而其他所有成绩则为FAIL

if score >= 50: print('PASS')else: print('FAIL')

由于没有elif语句,else会处理if没有捕获到的任何分数。当然,是否包含elif语句完全取决于你打算实现的逻辑。

逻辑运算符

到目前为止,每个if...elif语句都依赖于单一关系运算的结果。但通常情况下,在单个表达式中评估多个关系运算是非常有用的。例如,你可能想检查先前提到的球是否是红色的并且有刺。为此,你可以使用逻辑运算符,或者在另一个if语句中嵌套一个if语句,以根据多个条件的结果做出决定。

让我们修改之前的示例,以处理红色且有刺的球。你可以使用嵌套的if语句,如下面的伪代码所示:

if `ball is red`: if `ball is spiky`: `place in red & spiky bucket`

外部的if检查球是否是红色的,然后内部的if检查球是否有刺。

你也可以使用单一的if语句结合逻辑运算符and来做同样的事情:

if `ball is red` and `ball is spiky`: `place in red & spiky bucket`

and运算符在两边的表达式都为真时返回True

表 4-2 列出了 Python 的逻辑运算符,并提供了每个运算符的简要说明和示例。

表 4-2:逻辑运算符

运算符 描述 示例
and 如果两个操作数都为真,则返回True 2 > 1 and 4 > 3 返回 True
or 如果至少一个操作数为真,则返回True 2 > 1 or 4 < 3 返回 True
not True变为False,反之亦然 not 4 < 3 返回 True

现在让我们通过使用 and 运算符将两个表达式连接起来,以检查一个更为狭窄的条件。添加另一个 if 语句来检查学生的分数是否大于或等于 45 并且 小于 50,如果是,则显示 OFFER RETAKE

. . .if score >= 45 and score < 50: print('OFFER RETAKE')

这个条件包含了逻辑运算符 and。为了使其评估为 Truescore >= 45score < 50 都必须返回 True。将 score 值改为此范围内的数字,例如 46,并确认控制台显示 FAILOFFER RETAKE

检查无效输入

现在,让我们添加另一个 if 语句,使用 or 条件来处理任何超出有效范围(0 到 100)的分数。将此语句放在 if...elif 链的顶部,并将 A 语句改为 elif

. . .score = **105**1 if score < 0 or score > 100: print('INVALID SCORE')2 **elif** score >= 80 : print('A'). . .

现在,如果 score105,程序应该打印 INVALID SCORE。对于 or 运算符 1 来说,要评估为 True,它的两个操作数之一,score < 0score > 100,必须返回 True。尝试不同的分数值以测试代码。如果控制台显示无效的分数信息以及一个成绩,确保你已将第二个语句更改为 elif 2。

显示无效输入的消息

还有一个最后的改进空间。目前,如果用户输入一个 0 的分数,程序会将其判定为 FAIL。然而,0 的分数相对不常见,因此添加一个最终的 if 语句,显示一个警告消息,提示用户可能输入了无效的内容。

这是完整的评分程序:

. . .score = 0if score < 0 or score > 100: print('INVALID SCORE')elif score >= 80: print('A')elif score >= 65: print('B')elif score >= 50: print('C')else: print('FAIL')if score >= 45 and score < 50: print('OFFER RETAKE')if not1 score: print('WARNING: SCORE IS ZERO')

回想一下,当处理布尔值时,Python 将 0 解释为 False。这意味着如果 score 被赋值为 0,它会被评估为 False。然而,not 运算符 1 会反转这个布尔值,将其转换为 True,从而触发警告信息。你也可以使用 mark == 0 来测试相同的条件,这样更加明确且易于阅读,但这是一个很好的机会来展示 not 运算符的实际应用。请注意,这是一个独立的 if 语句,因此对于 0 的得分,控制台会同时显示 FAIL 和警告信息。

你可能会决定程序需要一些可点击的按钮和输入框。第十一章介绍了鼠标和键盘交互技术,你可以用它们为这种程序添加图形用户界面。

挑战 #3:四方任务

在这个挑战中,你将使用条件语句来判断一个点在四种颜色方格中的位置。请添加以下代码:

size(600, 600)noFill()noStroke()fill('#FF0000') # red quadrantrect(width/2, 0, width/2, height/2)fill('#004477') # blue quadrantrect(0, 0, width/2, height/2)fill('#6633FF') # violet quadrantrect(0, height/2, width/2, height/2)fill('#FF9900') # orange quadrantrect(width/2, height/2, width/2, height/2)

运行草图。显示窗口应该出现,平等地分为不同颜色的象限(图 4-5)。每一对 fill()rect() 行绘制了一个从显示窗口中心到每个角落的彩色方块。

f04005

图 4-5:四种颜色的网格

接下来,在右上象限放置一个单一的文本字符:

. . .
x = 400
y = 1001 txt = '?'fill('#FFFFFF')textSize(40)textAlign(CENTER, CENTER)2 text(txt, x, y)

txt值 1,一个问号,位于红色象限中(图 4-6)。text()函数 2 依赖于xy变量来控制该字符的位置。

f04006

图 4-6:将问号放置在红色(右上)象限中

您的挑战是编写条件语句,将?字符替换为 R(红色)、B(蓝色)、P(紫色)或 O(橙色),并放入适当的方格中,以匹配下面的颜色。

首先,插入一个if语句来管理 R(红色)条件:

. . .txt = '?'if x >= width/2: txt = 'R'fill('#FFFFFF'). . .

if语句将txt变量设置为'R',适用于显示窗口中心右侧的任何x值。运行草图,确认代码在红色象限中显示 R。如果仍然看到一个问号,确保已将if语句插入到fill('#FFFFFF')行之上。

现在,将y值设置为400,以将字符放置在橙色象限中。运行草图。它仍然是一个 R。要显示 O,您需要添加一个elif语句(以及一个逻辑and操作符)。一旦 O 正常工作,尝试将字符放置在另一个象限中,依此类推。图 4-7 显示了完成任务的四个截图;标题列出了相应的 x-y 坐标。

现在,您已经掌握了if...elif...else逻辑,您可以开始使用布尔表达式进行迭代。如果需要帮助,可以访问解决方案:github.com/tabreturn/processing.py-book/tree/master/chapter-04-conditional_statements/four_square/

f04007

图 4-7:从左上角开始顺时针:每个字母的 x-y 坐标为 R = (400, 100)、O = (400, 400)、P = (250, 485) 和 B = (38, 121)。

小结

在本章中,您学习了布尔数据类型、关系运算符以及如何编写布尔表达式,这些表达式与if语句一起工作,指示 Python 执行特定的代码行。您还探索了构造更复杂表达式的逻辑运算符,以及如何组合使用ifelifelse语句。

在第五章中,您将进一步控制流程,学习如何编写可以重复操作直到满足某些要求的程序。为了获得一些特别有趣的结果,您将为创作添加随机性。

第五章:迭代与随机性

在第四章中,你学习了如何为 Python 编程实现分支路径。在本章中,你将使用whilefor循环语句创建循环路径。循环语句重复动作,这样你就无需多次编写相同或相似的代码,从而减少了代码行数。换句话说,你可以使用更易于适应的代码更高效地解决问题。你将使用这些循环语句在 Processing 中生成视觉图案。

你还将学习如何将随机性应用到你的图案中,使其更加引人注目且不可预测。Processing 的random()函数非常有用,可以为你的形状函数生成随机化的参数,允许你创建不规则的设计。你还可以随机化控制流语句的条件,使得每次运行时你的代码都会有所不同。毫无疑问,随机性是创意编程工具集中最有用且最激动人心的工具之一,因为它使你能够编写出能够产生不可预测结果的程序。

迭代

在计算机编程中,迭代是重复一系列指令指定次数或直到满足某个条件的过程。举个例子,假设你要铺设地板。你从一个角落开始,放置一块瓷砖。然后你将另一块瓷砖放在它旁边,重复这个过程直到你到达对面的墙壁,然后向下移动一行继续。在这个场景中,放置一块瓷砖就是一次迭代。在许多迭代过程中,前一次迭代的结果定义了下一次迭代的起始点。

然而,像铺设瓷砖这样的任务可能是繁琐的工作。人类在推理和创造性思维方面非常出色,但如果没有足够的刺激,他们往往会失去对执行这种单调活动的兴趣。然而,计算机在执行重复性任务时,尤其是在涉及数字时,能够迅速且精确地完成。

使用迭代绘制同心圆

要开始在 Processing 中探索迭代,创建一个新的草图并将其保存为concentric_circles。然后添加以下代码:

size(500, 500)background('#004477')noFill()stroke('#FFFFFF')strokeWeight(3)circle(width/2, height/2, 30)circle(width/2, height/2, 60)circle(width/2, height/2, 90)

每个circle()函数的 x-y 坐标都放置在显示窗口的中心。第一个圆是最小的,直径参数为30;每个后续的圆直径比前一个大 30 像素。程序逐行运行每个circle()函数,向着一个充满同心圆的显示窗口(图 5-1)推进。

f05001

图 5-1:使用三个circle()函数绘制的三圆

然而,要填满整个窗口,你需要写更多的circle()行代码。你可以使用 Python 的while循环来迭代运行这些circle()函数,而不是手动添加circle()函数。

while 循环

while循环是一种控制流语句,类似于if语句。关键区别在于while会继续执行它下方缩进的代码直到条件不再成立。

在你的同心圆草图中,通过使用'''进行多行注释,将circle()行注释掉,并添加一个基本的while循环结构:

. . .'''circle(width/2, height/2, 30, 30)circle(width/2, height/2, 60, 60)circle(width/2, height/2, 90, 90)'''
i = 0while i < 24: print(i)

i变量被定义为循环计数器,控制while语句的迭代。对于while表达式,i等于 0,因此小于 24。与仅执行一次print()函数的if语句不同,while会重复执行print语句,直到i的值达到 24——而在此情况下,永远不会达到。

运行草图时,控制台将打印出一连串的0(图 5-2)。

f05002

图 5-2:控制台列出了无尽的零。

这段代码使你的程序崩溃,进入了一个无限循环!要退出程序,请点击停止按钮。处理可能需要一些时间才能响应。变量i保持为 0,且i < 24条件永远无法达到False,因此循环无法结束。

为了纠正这个问题,在每次while循环迭代时将i加 1:

. . .while i < 24: print(i) i = i + 1

这行代码表示循环计数器i等于其自身加 1。在第一次迭代时,i为 0,满足小于 24 的条件,程序打印0,然后将i加 1,开始下一次迭代。在下次迭代中,i为 1,依然小于 24,程序打印1,将i加 1,并再次执行这个过程。只要i < 24的条件为True,迭代将继续。当i达到 24 时,程序将退出循环并执行while块之后的其他代码。

请注意,输出永远不会达到 24(图 5-3),因为while条件语句中写的是“i小于 24”,而不是“i小于或等于 24”。

要绘制 24 个圆形,请在循环内放置circle()函数:

. . .while i < 24: print(i) circle(width/2, height/2, 30*i) i = i + 1

f05003

图 5-3:控制台显示 0 到 23,但不显示 24。

为了避免绘制 24 个大小完全相同、位置相同的圆形,可以将i作为circle()直径参数的乘数。在第一次迭代时,直径参数等于30*0。因此,第一个圆形位于显示窗口的正中心,直径为 0,因此不会渲染出来(图 5-4)。

其他 23 个圆形足以填满 500 × 500 像素的区域。通过修改while语句中的数字,你可以绘制任意数量的圆形(多或少)。

f05004

图 5-4:图形现在有 24 个圆形(其中一个不可见,另一些部分被裁剪)。

for 循环

Python 的 for 循环会执行指定次数的代码块。不同于依赖条件表达式的 while 循环,for 循环是对一个序列进行迭代。序列 是一组值的集合;例如,字符串数据是字符的序列。Python 的 列表 是特别灵活的序列,关于列表的内容我将在第七章讲解。为了在本节的 for 循环中生成序列,你将使用 range() 函数。

当你在进入循环之前已经确定了所需的迭代次数时,for 循环比 while 循环更合适。一般来说,for 循环更简洁,且不会触发无限循环。当 while 循环或 for 循环都能满足需求时,选择 for 循环。

理解 for 循环的最简单方法之一是将你已经写过的使用 while 语句的代码进行转换。通过使用 文件另存为,将 concentric_circles 保存为一个名为 for_loop 的新草图。将 while 循环的部分注释掉,并添加以下 for 循环:

. . .'''1 i = 0while i < 24: print(i) circle(width/2, height/2, 30*i)2     i = i + 1'''3 for i in range(24): print(i) circle(width/2, height/2, 30*i)

while 循环版本中,记得你必须定义 i 变量作为循环计数器。每次 while 块迭代时,你还必须递增 i 以避免进入无限循环。for 语句则不需要定义和管理一个单独的计数器变量。

因此,i = 0 1 就不再需要,也不再需要嵌套语句来递增它 2。相反,range() 函数接受 24 作为参数,生成从 0 到不包括 24 的序列,控制 for 循环 3 的迭代行为。在第一次迭代时,i 等于 0,即序列中的第一个值。每次迭代后,range() 序列中的下一个值将赋给 i。当 i 达到 23 时,for 块会执行最后一次,然后 Python 退出循环。运行草图以确认显示窗口与 图 5-4 看起来相同。

range() 函数最多可以接受三个参数。分别提供起始值和结束值两个参数:

. . .for i in range(10, 13): print(i) circle(width/2, height/2, 30*i)

在这个例子中,circle() 函数应该执行三次,分别是 i = 10, i = 11 和 i = 12。运行草图以查看结果(图 5-5)。

你应该能看到三个同心圆。

f05005

图 5-5:range(10, 13) 的结果

现在使用三个范围参数来分别表示起始、结束和步长。步长 是序列中每个整数之间的差值:

. . .for i in range(3, 13, 3): print(i) circle(width/2, height/2, 30*i)

在这个例子中,circle() 函数应该执行四次,分别是 i = 3, i = 6, i = 9 和 i = 12。结果应该是四个间距增大的环(图 5-6)。

f05006

图 5-6:range(3, 13, 3) 的结果

尝试不同的范围参数,观察圆形是如何被影响的。

挑战 #4:创建线条图案

在这个挑战中,使用line()函数和每个图案一个for循环,重新创建图 5-7 中展示的三个图案。如果你的代码产生的结果与示例稍有不同,也不要担心,只要基本图案保持不变即可。

如果你不确定从哪里开始,这里有一些线索可以帮助你处理每个图案:

  • 左侧的图案类似于同心圆,只是它有 12 条对角线。

  • 对于中间的图案,行间距随着每次for循环迭代增加 1.5 倍。定义一个额外的变量可能会有所帮助。

  • 右侧的图案需要在for循环中嵌套if...else结构。你可以考虑使用第一章中描述的取余(%)运算符来判断i是奇数还是偶数。

如果你需要帮助,可以在github.com/tabreturn/processing.py-book/tree/master/chapter-05-iteration_and_randomness/for_loop_patterns/找到解决方案。

f05007

图 5-7:三种for循环图案

breakcontinue语句

循环提供了一种高效的方式来自动化和重复任务。不过,有时你可能需要提前退出循环。例如,当你绘制一系列同心圆来填充显示窗口时(如前面的任务所示),如果圆形在range()值序列耗尽之前到达显示窗口的边缘,你可能想要break循环。如果 Python 在forwhile循环中遇到break语句,它会立即终止该循环。循环终止后,程序会照常继续执行。

有时你需要终止一次迭代(而不是整个循环),让 Python 立即开始下一次迭代。为此,可以使用continue语句。

让我们看一个简单的示例,比较普通的循环、带有break语句的循环和带有continue语句的循环。无需编写任何代码。图 5-8 展示了三条虚线,从左到右使用每种循环类型绘制。

淡蓝色(上方)虚线的循环如下所示:

for i in range(20, width, 20): fill('#0099FF') circle(i, 75, 10)

每次迭代时,circle()函数绘制一个新的点,将其放置在前一个点右侧 20 像素的位置。第一个点的 x 坐标是 20;当虚线达到显示窗口的width时,循环完成。这个循环不关心两个垂直的红色带,并会直接穿过它们绘制点。

f05008

图 5-8:使用不同循环绘制虚线

橙色(中间)虚线的循环如下所示:

for i in range(20, width, 20): if red(get(i, 150)) == 255: break fill('#FF9900') circle(i, 150, 10)

get() 函数接受一个 x-y 坐标并返回该位置的像素颜色;要提取该像素的红色值,你需要将 get() 函数包裹在 red() 函数中。这会根据 RGB 混合返回一个介于 0 和 255 之间的红色值,对于任何在亮红色带中的像素(#FF0000),其值为 255。循环会在绘制点之前检查是否有红色像素;如果检测到,break 语句会终止循环。由于 break 语句会立即退出循环,fill()circle() 函数在最后一次迭代时不会绘制任何点。

绿色(底部)虚线的循环如下所示:

for i in range(20, width, 20): if red(get(i, 225)) == 255: continue fill('#00FF00') circle(i, 225, 10)

这个循环会在绘制点之前检查是否有红色像素;如果检测到像素,continue 语句会立即终止当前循环的迭代,并开始下一次迭代,跳过 fill()circle() 函数。

随机性

随机性是计算机编程中的一个重要概念,因为它在加密学中的应用。此外,随机性被编程应用于从视频游戏到模拟到赌博软件的各个领域。然而,计算机生成的随机数并不是真正的随机,因为它们是通过特定的算法生成的。如果你知道生成“随机”数所用的算法和条件,你就可以预测序列中的模式。因此,计算机只能通过生成伪随机数来模拟随机性,这些数并不是真正的随机数,但在统计上足够接近实际的随机数。

在本节中,你将使用 Processing 的 random()randomSeed() 函数来生成伪随机值。利用这些随机值,你将绘制出比使用预定义值时更有趣的图案。

random() 函数

每次调用 Processing 的 random() 函数时,它会在指定的范围内生成一个意想不到的值。为了开始实验随机性,创建一个新的草图并将其保存为 random_functions。添加以下设置代码:

size(600, 250)background('#004477')noFill()stroke('#FFFFFF')strokeWeight(9)

新的草图有一个蓝色背景。很快,你将绘制点;点的大小受到 strokeWeight() 函数的影响。

random() 函数最多可以接受两个参数。如果只有一个参数,表示你定义了一个上限:

print(random(5))

这段代码会显示一个范围从 0 到但不包括 5 的随机浮动值。

两个参数分别表示上限和下限:

print(random(5, 10))

这一次,控制台会显示一个范围从 5 到但不包括 10 的随机浮动数。

如果你想要一个随机整数,可以将 random() 函数包裹在 int() 中。这会通过去掉小数点及其后的所有内容,将浮动数转换为整数:

print(int(random(5, 10)))

图 5-9 显示了你可以预期看到的内容。当然,由于值是随机的,控制台输出在你的计算机上会有所不同,每次运行草图时也会有所不同。

f05009

图 5-9:尝试不同的 random() 参数

接下来,让我们生成 50 个随机值。与其在控制台区域打印一长串数据,不如将它们作为一系列共享 y 坐标的点进行绘制。添加以下代码:

. . .for i in range(50): point(random(width), height/2)

这个 point() 函数使用 random() 函数来定义其 x 坐标。y 坐标始终为 height/2。每次运行草图时,点的位置应该会有所不同(图 5-10)。

f05010

图 5-10:沿一条直线分布的随机值

现在将 range 参数从 50 改为 500,并使用随机的 x 和 y 坐标绘制点:

. . .for i in range(**500**): point(random(width), **random(height)**)

结果应该是一个显示窗口,里面填充了 500 个随机位置的点(图 5-11)。

f05011

图 5-11:用随机摆放的点填充显示窗口

每次运行草图时,它都会生成一个(略微)不同的排列。

随机种子

在图 5-10 和图 5-11 中,Processing 从伪随机的 序列 中选择坐标。这个伪随机序列本身依赖于一个 随机种子,这是一个随机函数根据不可预测的因素(如按键时间)选择的初始数字。例如,你可能在上一秒的时刻过去 684 毫秒时按下了最后一个键。对于 0 到 9 之间的随机数,你的计算机会抓取 684 的最后一位数字(即 4)。随机种子决定了你从第一次 random() 调用中得到的结果以及后续所有调用的结果。

你可以使用 Processing 的 randomSeed() 函数手动设置随机种子。将范围参数改为 10,并在工作草图的最顶部插入一行 randomSeed()

randomSeed(213)size(600, 250). . .for i in range(**10**): point(random(width), random(height))

这个 randomSeed() 函数接受一个单一的参数,任何你选择的整数,不过在本例中我们使用 213。与 500 个点的(图 5-11)版本不同,该版本没有定义随机种子,每次运行代码时都会产生相同的模式,在任何执行它的计算机上都是如此。

这种确保程序每次运行时生成相同伪随机数序列的能力在许多应用中非常有用。例如,假设你开发了一个平台游戏,关卡中包含随机摆放的障碍物。不需要手动摆放障碍物可以节省大量时间。然而,你发现某些伪随机数序列生成的关卡比其他的更具吸引力。而且,生成的关卡难度不同,所以你需要控制玩家通过关卡的顺序。如果你知道哪些种子值生成了每个关卡,你就可以仅通过一个整数按需重现任何一个关卡。

在下一部分,你将结合 for 循环和 random() 函数来创建有趣的瓷砖排列。

Truchet 瓷砖

塞巴斯蒂安·特鲁谢(Sébastien Truchet,1657–1729),一位法国多米尼加修道士,活跃于数学、水利学、图形学和排版学等领域。在他众多的贡献中,他开发了一种利用瓷砖创建有趣图案的方案,这些瓷砖后来被称为Truchet 瓷砖。原始的 Truchet 瓷砖是方形的,并通过对角线将其对角角落分割。这个瓷砖可以以 90 度的倍数旋转,产生四种变体,如图 5-12 所示。

f05012

图 5-12:一块 Truchet 瓷砖,展示了其四种可能的方向

这些瓷砖排列在一个方形网格上,可以是随机的或根据某种模式排列,以创建美观的设计。图 5-13 展示了四种可能的排列,包括一个随机的瓷砖排列(右下角)以及一些有序的排列方法。

f05013

图 5-13:四种 Truchet 瓷砖布局

接下来,你将使用图 5-14 中展示的四分之一圆 Truchet 瓷砖,并以其两种可能的方向进行排列。

让我们运用你在本章中学到的循环和随机性技术,使用这种瓷砖创建不同的图案。创建一个新的草图,并将其保存为 truchet_tiles。添加以下设置代码:

size(600, 600)background('#004477')noFill()stroke('#FFFFFF')strokeWeight(3)for i in range(1, 145): arc(0, 0, 50, 50, 0, PI/2) arc(50, 50, 50, 50, PI, PI*1.5)

新的草图有一个蓝色背景。你绘制的每个形状将没有填充,只有 3 像素宽的白色轮廓线。这是为了绘制图 5-14 中展示的四分之一圆设计。每块瓷砖的大小是 50 × 50 像素,因此显示窗口可以正好放下 12 列(600 ÷ 50)和 12 行。因此,填满显示窗口需要 144 块(12 × 12)瓷砖,这就是 range(1, 145) 的由来。

f05014

图 5-14:四分之一圆 Truchet 瓷砖

运行草图。一个瓷砖应出现在左上角(图 5-15)。

f05015

图 5-15:所有 144 块瓷砖放置在左上角

实际上,在图 5-15 中,你看到的是所有 144 块瓷砖放置在同一个位置!

要控制列和行的位置,使用 colrow 变量。根据粗体代码修改你的脚本:

. . .**col = 0****row = 0**for i in range(1, 145): arc(**col**, **row**, 50, 50, 0, PI/2) arc(**col+**50, **row+**50, 50, 50, PI, PI*1.5) **col += 50**

每次循环迭代时,col 变量(瓷砖的 y 坐标)增加 50。结果应当是每个瓷砖放置在其前一个瓷砖的右边,如图 5-16 所示。

f05016

图 5-16:剩余的 132 块瓷砖位于右边缘之外。

然而,有一个问题:程序不知道何时返回左边缘并开始新的一行。相反,瓷砖溢出,延伸到右边缘,超出了可见范围。

为了纠正这一点,在循环中嵌套一个 if 语句:

. . .for i in range(1, 145): . . . if i % 12 == 0: row += 50 col = 0

i % 12对于任何能够被 12 整除的i值都会返回 0。换句话说,如果一个除以 12 的操作余数为 0,就表示你刚刚放置了另外 12 个瓷砖。此时,row变量会增加 50,col会重置为 0。下一个瓷砖会被放置在新一行的开头,这将导致显示窗口充满瓷砖(图 5-17)。

f05017

图 5-17:充满四分之一圆形 Truchet 瓷砖的显示窗口

为了增加趣味性,通过添加以下if...else结构来随机化每个瓷砖的方向:

. . .for i in range(1, 145): if int(random(2)1): arc(col, row, 50, 50, 0, PI/2) arc(col+50, row+50, 50, 50, PI, PI*1.5) 2 else: arc(col+50, row, 50, 50, PI/2, PI) arc(col, row+50, 50, 50, PI*1.5, 2*PI) col += 50 . . .

random(2)函数会返回一个从 0 到不包括 2 的浮点值。因此,将结果通过int()转换为整数,便会得到 0 或 1。这就像是抛硬币,每次迭代时会决定选择哪种瓷砖方向。因为这个“抛硬币”操作返回的是一个布尔兼容的值——0 或 1——它可以作为if语句的条件,完全不需要关系运算符。如果抛硬币的结果是 0,else代码 2 将会运行,因为0等同于False(而if只在True时运行)。

每次运行草图时,显示窗口都会呈现不同的图案(图 5-18)。

f05018

图 5-18:随机排列的四分之一圆形 Truchet 瓷砖

如果你曾经玩过策略游戏Trax,这个图案应该很熟悉。另一个基于瓷砖的策略游戏Tantrix使用了 Truchet 瓷砖的六边形变体。当然,瓷砖的种类远不止 Truchet 这一种。你可以尝试添加填充、将半圆形替换为对角线、增加额外的瓷砖或添加有关瓷砖之间可以相邻的规则(图 5-19)。如果你在寻找有趣的项目,许多瓷砖图案可供灵感参考。

f05019

图 5-19:Truchet 瓷砖的变体

你可以在github.com/tabreturn/processing.py-book/tree/master/chapter-05-iteration_and_randomness/truchet_tiles_variations找到一些 Truchet 瓷砖变体的代码。

随着你的程序变得越来越复杂,你会发现有多种方法可以编码出相同的结果。例如,你可以通过在一个循环内使用另一个循环,利用带步长参数的range()函数,或是各种组合方式来铺设四分之一圆形的 Truchet 瓷砖。在 Github 上的 Truchet 瓷砖变体中,你会找到一个名为loop_within_a_loop的示例,它采用了这种方法。现在你已经理解了控制流,你可以开始思考如何优化算法以提高可读性和效率。

总结

您现在已经学习了迭代以及如何使用whilefor语句编写循环;这让您能够用更少的代码行完成更多的任务,且代码更加灵活。循环将在本书的后续章节中不断出现,为您提供更多掌握它们的机会。

本章还介绍了随机性,它在各种计算应用中都很有用,包括创意编程。Processing 的random()函数生成伪随机数序列,您可以通过使用随机种子来控制这些序列,从而确保每次运行草图时产生相同的值序列。

下一章将介绍运动。您将学习如何为您的 Processing 草图添加运动,同时也会学习变换函数,作为高效的方式来移动、旋转和缩放您的元素,特别适用于一组形状。

第六章:运动与变换

将运动应用于生物和无生命物体的图形,可以赋予它们个性。弹跳的动画暗示着俏皮;精确的运动意味着强度,而慢动作则可以暗示沉重。这些技巧被广泛应用于电影、动画、舞蹈编排,当然,还有你最喜欢的皮克斯动画片。但这还不是全部,运动在界面设计中也非常普遍,比如微妙的按钮悬停效果,或者在内容加载时出现的复杂旋转图形。

在本章中,你将通过编写运动和变换函数的代码让物体动起来。你将学习如何使用变换函数操控坐标系统,使得移动、旋转和缩放元素变得更加简单。此外,你还将学习如何通过使用setup()draw()函数来构建一个动画的 Processing 草图。运动从字面上为你的 Processing 草图增加了一个新的维度——时间。

感知运动

首先,考虑一下运动是如何被感知的。大脑每秒钟从视网膜获取许多次快照。只要显示屏每秒能够显示超过大约 10 到 12 帧的静态图像,观众就会体验到平滑流畅的运动错觉。更高的帧率会显得更加平滑。

稍作停顿,注意图 6-1 中的两个圆圈。

f06001

图 6-1:两个圆圈(左侧和右侧定位)

如果你仅展示左边的圆圈四秒钟,再展示右边的圆圈四秒钟,循环这个序列无限次(见图 6-2),这将是一个有效的帧率,约为每秒 0.25 帧(即 0.25 fps)。结果,大多数观察者会同意,这将呈现出一对交替出现的图像,显示圆圈在两个不同位置之间变化。

f06002

图 6-2:显示交替的圆圈

然而,当帧率加速到大约 2.5 帧每秒(快 10 倍)时,观察者将开始将序列解读为一个在两个点之间弹跳的圆圈——就好像圆圈在中间的空隙中移动一样。这种错觉被称为贝塔运动。进一步提高帧率时,两个圆圈将同步闪烁。从这个实验中,你可以看到帧率不仅影响物体的移动速度,还影响你对物体运动的感知。

请注意图 6-3 中圆圈的编号。

现在,假设你想要为此做动画。按照编号来确定顺序,每一帧移除一个圆圈。在第一帧,移除标记为 0 的圆圈。在第二帧,替换圆圈 0 并只移除圆圈 1。继续围绕环形路径进行此操作,并使动画循环播放。每一帧移除一个连续的圆圈,结果是一个以顺时针方向移动的间隙(图 6-4)。

f06003

图 6-3:按顺时针顺序编号的圆圈环

f06004

图 6-4:动画中的圆圈环

如果你以 1 帧每秒(fps)的速度运行动画,圆圈刚好出现在间隙前面,似乎跳入了空缺的圆圈位置(图 6-5)。

f06005

图 6-5:以 1 fps 的速度,接下来的圆圈似乎跳进了空缺中。

然而,在 25 帧每秒(fps)下,一个快速移动的虚拟白点似乎遮挡了其下方的圆圈,因为它在环形路径上疾驰——这种现象被称为菲现象(图 6-6)。

现在你已经准备好构建一个 Processing 草图,除了引入 Processing 的动画功能外,还可以让你实验这些现象。

f06006

图 6-6:以 25 fps 的速度,一个虚拟的白点似乎遮挡了圆圈。

为 Processing 草图添加运动

Processing 允许你选择在显示窗口中绘制一次或多次。对于动画,你会使用后者的方法。为了让一个对象移动,你需要在每一帧中调整它的位置——如果调整得足够迅速、增量足够小,结果就是平滑、流畅的运动。

draw()setup() 函数

要让 Processing 多次绘制某个对象,你需要通过使用setup()draw()函数来构建你的代码。在这两个函数下面,你可以嵌套书中到目前为止讲解的任何函数或语句。正如图 6-7 所示,你的代码放置位置取决于你希望它执行的时机。

f06007

图 6-7:为运动构建代码结构

任何def关键字后面都会跟着函数名、括号和冒号。第九章会更详细地介绍def,但现在只需知道,任何缩进在def下方的代码都属于该函数。

setup()代码在启动时只运行一次,通常包括像size()函数这样的内容以及定义环境属性的其他代码。我稍后会更详细地讲解draw(),但首先,创建一个新的草图,保存为perceiving_motion,然后添加以下代码:

def setup(): size(500, 500) background('#004477') noFill() stroke('#FFFFFF') strokeWeight(3)

这段代码几乎与你之前设置的每一个草图相似,唯一不同的是def setup()这一行。每当你打算使用draw()函数时,你也必须使用setup()。现在添加draw()函数:

. . .def draw(): print(frameCount)

Processing 在每个新的帧下调用draw()函数中的代码。frameCount是一个系统变量,包含自开始草图以来显示的帧数。每当新帧到来时,draw()函数会调用print()函数,从而在控制台显示当前的帧计数。

默认情况下,draw()以大约 60 帧每秒(fps)执行。然而,随着动画复杂度的增加,帧率可能会下降,因为你的计算机需要应对更高的需求。通过使用frameRate()函数(在setup()块内)调整帧率,并向draw()添加条件,仅在偶数帧上进行打印:

def setup(): . . . frameRate(2.5)def draw():  if  frameCount % 2 == 0: print(frameCount)

frameRate设置为2.5,表示每秒绘制两次半线;这意味着每一帧的持续时间是 400 毫秒(0.4 秒)。由于print行在每一秒的第 2 帧执行,因此每 800 毫秒(图 6-8)控制台中会出现一行新的输出。

f06008

图 6-8:在每个偶数帧上打印帧计数

若要仅在每个偶数帧上绘制圆形,可以使用以下circle()行:

. . .def draw(): if frameCount % 2 == 0: circle(420, 250, 80)

现在运行草图。你可能会惊讶地发现,圆形并没有闪烁开关(图 6-9)。

f06009

图 6-9:圆形不会“闪烁”。

圆形在奇数帧上不消失的原因是,在 Processing 中,绘制的内容会保持不变。每个偶数帧时,程序会在现有的“堆叠”上绘制另一个圆。setup()函数中的background()颜色在程序开始时执行一次,填充显示窗口为蓝色,形成这个持久性排列的最底层。为了在绘制下一个帧之前“擦除”每一帧,你可以在绘制所有内容之前重新绘制背景。

background('#004477')行复制到你的草图的draw()部分:

. . .def draw(): background('#004477') if frameCount % 2 == 0: circle(420, 250, 80)

新的background()行在每一帧之前清除画面。确保它位于if语句之前。在大多数情况下,background()函数通常位于draw()的顶部,以避免清除当前帧中的其他形状。

测试代码。结果应该是一个闪烁的圆。

为了重新创建之前的圆环实验(图 6-3),将现有的if语句替换为一系列的if语句:

. . .def draw(): background('#004477') hide = frameCount % 81 if hide != 0: circle(250, 80, 80) if hide != 1: circle(370, 130, 80) if hide != 2: circle(420, 250, 80) if hide != 3: circle(370, 370, 80) if hide != 4: circle(250, 420, 80) if hide != 5: circle(130, 370, 80) if hide != 6: circle(80, 250, 80) if hide != 7: circle(130, 130, 80)

当前帧计数除以8的余数赋值给hide变量。每个if语句将绘制一个单独的圆,前提是该圆没有被标记为隐藏。例如,在第 16 帧时,hide等于0,因为 16 能够被 8 整除。在第 15 帧时,hide等于7,因为 15 除以 8 的余数是 7。在第 17 帧时,hide等于1。结果是一个从 0 到 7 的数字流,然后从 0 重新开始。

运行草图。注意间隙如何在圆圈周围移动。在当前的帧率 2.5 fps 下,间隙前方的圆圈似乎跳入了空缺圆圈留下的空白处。但当帧率调整为 25 fps 时,一个类似幽灵的背景色点似乎会在它绕环圈快速移动时遮住下面的圆圈。

全局变量

全局变量是指你可以在程序中的任何地方访问的变量。到目前为止,书中你定义的几乎每个变量都是全局变量。你需要更多地了解全局变量,以便在多个帧之间管理数据。

全局变量是在任何函数定义(以def开头的缩进代码块)外声明的,通常位于代码的顶部。例如,你在setup()draw()外声明的任何变量都会自动成为全局变量。相反,任何在这两个函数的缩进行内声明的变量只在该函数内可访问。

作为这种行为的示例,创建一个新的草图并将其保存为global_variables。添加以下代码:

def setup(): 1 y = 1def draw(): 2 print(y)

y变量 1 是在setup()函数内声明的。因此,y仅在setup()代码块的缩进行内可访问。y变量的作用域因此被认为是局部的,限定在setup()中。在编程中,作用域是指可以访问变量(或其他实体)的区域。在这种情况下,运行草图会产生一个错误(图 6-10),因为你尝试从draw()函数中访问并打印变量y 2。

f06010

图 6-10:draw()函数无法访问在setup()中声明的y变量。

另外,你也可以将y = 1这一行移动到setup()函数外部,这样它就处于全局作用域;这允许任意一个函数读取它。将这一行移动到代码顶部,并在你移出的位置插入一个pass语句:

y = 1def setup(): passdef draw(): print(y)

draw()函数现在可以访问y,因为它已经在setup()外部声明。pass语句是一个空操作——也就是说,当它执行时什么都不会发生。你需要包含一个pass语句,因为 Python 不允许空的函数定义。这使得pass成为任何尚未编写代码的有用占位符。运行草图时,控制台应该会打印出无尽的1

你可以通过在局部范围内使用另一个同名变量来覆盖全局的y变量——在这种情况下,另一个名为y的变量。对你的代码进行以下调整:

y = 1def setup(): 1 y = 0 2 print(y)def draw(): 3 print(y)

setup()函数先运行—仅运行一次—并且它的print第 2 行显示0。这是因为在setup()函数中,你将y定义为0。外部(全局)y仍然等于 1,并且它被认为是被setup()内部(局部)y变量所遮蔽draw()代码在setup()代码之后执行,并且在每个新帧中,都会向控制台打印1。运行草图,迅速停止它,然后向上滚动控制台输出。第一行显示的是0;从那以后,都是1(见图 6-11)。

f06011

图 6-11:全局y变量被y = 0遮蔽。

接下来,移除y = 0行,并添加代码,尝试在每帧中将全局y变量增加1

. . .def draw(): y += 1 print(y)

虽然你可以读取(或遮蔽)任何全局变量,但写入或重新赋值需要额外的代码。因此,这段代码应该会导致 Processing 显示错误(见图 6-12)。

f06012

图 6-12:draw()函数无法重新赋值给y

这就是global语句有用的地方。编辑你的代码,在draw()块的顶部插入global y行:

. . .def draw(): global y y += 1 print(y)

全局y变量现在与draw()的局部作用域绑定,你可以根据需要修改它。运行草图。全局y变量现在应该在每个新帧中递增 1(见图 6-13)。

f06013

图 6-13:全局y变量在每个新帧中递增 1。

全局变量允许你轻松地跟踪和更新帧之间的值,这对于动画对象尤其有用。添加一个移动的圆圈,其 y 坐标由y变量控制:

y = 1def setup(): print(y) size(500, 500) noFill() stroke('#FFFFFF') strokeWeight(3)def draw(): . . . background('#004477') circle(height/2, y, 50)

我将大小、填充和描边属性放在了代码的setup()部分。由于描边和填充在整个动画过程中保持不变,因此无需在draw()中重复应用这些属性。圆圈的 y 坐标,由变量y表示,随着帧的推进而将圆圈向下移动。在图 6-14 中,已经添加了运动轨迹以传达运动方向。

f06014

图 6-14:圆圈从显示窗口的顶部向下移动。

当圆圈到达显示窗口的底部时,它会继续向下超出视野,越过底边。

保存帧

Processing 提供了saveFrame()函数,用于将帧保存为图像文件。每当你的草图调用saveFrame()时,它会将一个标记图像文件格式(TIFF)图像保存在草图文件夹中。你会希望在draw()函数的末尾放置这个调用,以确保捕获当前帧中渲染的每个形状。例如,假设你将以下代码添加到draw()函数中:

. . .def draw(): . . . if frameCount % 100 == 0: saveFrame() square(10, 10, 100)

每当动画遇到第 100 帧时,新的图像文件会出现在你的草图文件夹中。这个图像文件的命名规则是screen-后面跟着四位数的帧数;在需要时,帧数会用前导零填充,如图 6-15 所示。因为saveFrame()square()行之前执行,所以方块出现在动画的每一帧中,但永远不会出现在保存的图像文件中。

f06015

图 6-15:saveFrame()函数生成一个以帧数命名的图像文件。

如果你想将文件保存为除 TIFF 外的其他图像格式,如 JPG、PNG 或 TARGA,可以在文件名参数中包含相关的扩展名:

 . . . saveFrame('frame.png')

在这种情况下,你会使用相同的文件名保存每个图像,这对于捕捉单个帧是可以的,但当你多次调用相同的saveFrame()函数时,会导致文件被覆盖。然而,你可以在文件名中加入一系列哈希符号,使得帧数出现在文件名中。以下代码会在每次保存时生成一个独一无二的 PNG 文件:

 . . . saveFrame('frame-####.png')

Processing 会将哈希符号替换为帧数,并在必要时用前导零填充帧数。

挑战 #5:DVD 屏幕保护程序

在这个任务中,你将结合setup()draw()、全局变量和if语句来为一个物体制作动画,使其在显示窗口的边缘反弹。

DVD 播放器通常会有一个反弹的 DVD 标志作为屏幕保护程序(图 6-16),它会在一定时间的非活动后出现。你可能在其他设备上见过这种变化,尽管它们的图形不同。有趣的是,人们经常会盯着这个无意义的动画,希望看到标志完美地停在屏幕的角落。

f06016

图 6-16:标志在屏幕的边缘反弹。

创建一个新的草图,并将其保存为dvd_screensaver。添加以下代码:

y = 100
yspeed = 2def setup(): size(800, 600) fill('#0099FF') textSize(50)def draw(): 1 global y, yspeed background('#000000') 2 y += yspeed text('DVD', 100, y3)

这段代码与之前使用圆形的例子类似(图 6-14)。在这个实例中,你需要加入一个yspeed变量。要为多个变量使用单一的global语句,可以用逗号分隔它们。1. 每当新的一帧出现时,程序会将yspeed加到y变量上 2,这个y变量作为 DVD 文字的 y 坐标 3。运行草图时,标志应该会直接向下移动(图 6-17),很快会超出显示窗口的底边。

f06017

图 6-17:DVD 文字向下移动。

为了让标志从显示窗口的底边反弹,添加以下if语句:

. . .def draw(): . . . if y > height: yspeed *= -1

y变量超出显示窗口的height时,yspeed会乘以-1,使得标志朝相反的方向移动。运行草图时,标志应该会在触碰到底部边缘时反弹。

要让标志沿对角线移动,添加一些x值:

. . .
x = 100
xspeed = 2. . .def draw(): global y, yspeed, **x, xspeed** background('#000000') y += yspeed x += xspeed text('DVD', **x**, y) . . .

在这里,你已将之前使用 yyspeed 变量所做的一切复制到了 text() 函数的 x 参数中。现在,标志应该可以在垂直和水平方向上移动了。运行草图(图 6-18)。

f06018

图 6-18:斜着移动的 DVD 文本在右下角附近反弹。

当标志从底边反弹时,yspeed 被反转,但xspeed 不变。这是你期望的行为,但随后标志穿越了右边缘。相反,标志必须从它遇到的每个边缘反弹。你的挑战是完成这个任务。如果你需要帮助,可以访问解决方案:github.com/tabreturn/processing.py-book/tree/master/chapter-06-motion_and_transformation/dvd_screensaver/

变换

Processing 的变换函数通过使用平移、旋转、缩放和剪切操作提供了方便的方式来操控元素(图 6-19)。你可以将变换应用于单个形状、元素组或整个绘图空间。

f06019

图 6-19:从左到右:平移、旋转、缩放和剪切变换

假设你想要将一个星形(如图 6-20 所示)顺时针旋转。这个星形由一系列 vertex() 函数定义的顶点组成;每个顶点的位置由一对 x-y 坐标表示。

f06020

图 6-20:旋转一个星形

计算每个顶点的新位置需要一个矩阵。你可以把矩阵看作是一个数字表格。对于不同的变换,你可以通过加、减或乘以每对 x-y 坐标和变换矩阵来进行操作。在星形旋转的情况下,矩阵操作大致如下所示:图 6-21。方括号中标记为顶点xy值表示给定顶点的坐标对;这与变换矩阵相乘,用以计算新的旋转后的顶点位置。结果方括号中的方程揭示了矩阵运算的过程。

f06021

图 6-21:旋转的变换矩阵

如果矩阵运算看起来有些困惑,不用担心;Processing 会悄悄为你处理这一切。

在接下来的部分中,你将学习 translate()rotate()scale()shearX()shearY() 函数。你还将看到如何使用 pushMatrix()popMatrix() 函数来将变换应用于选定的元素组。

Processing 变换函数

创建一个新的草图,并将其保存为transformation_functions。在草图的文件夹内,创建一个data子文件夹,然后按照以下步骤操作:

  1. 打开你的网页浏览器并访问github.com/tabreturn/processing.py-book/

  2. 导航到chapter-06-motion_and_transformation

  3. 下载grid.pnggrid-overlay.png文件。

  4. 将两个文件放入你的data子文件夹中。

添加以下设置代码:

size(800, 800)noFill()noStroke()
grid = loadImage('grid.png')image(grid, 0, 0)
grido = loadImage('grid-overlay.png')

grid变量和image()语句加载并显示grid.png图形。grid-overlay.png文件被加载到grido变量中,但还没有在显示窗口中渲染;你将在后续任务中显示它。

translate()

translate()函数接受两个参数:x 偏移量和 y 偏移量。通常,坐标(0, 0)表示显示窗口的左上角。这个点被称为原点。使用translate(),你可以重新定位坐标系,这会移动原点,并影响你之后绘制的所有内容。

transformation_functions代码中添加translate()函数,并通过新的image()语句显示 grid-overlay 图形。

. . .translate(150, 100)image(grido, 0, 0)

translate()函数将整个坐标系水平移动 150 像素,垂直移动 100 像素。image()函数在(0, 0)位置绘制 grid-overlay 图形——这是一种浅蓝色版本的第一个网格图像。grid-overlay.png图形具有透明背景,因此你应该能够看到grid.png文件透过它显示。运行草图以确认输出与图 6-22 相符。

f06022

图 6-22:显示在其上方的经过变换的 grid-overlay 图像的网格图

x-y 坐标(0, 0)不再与显示窗口的左上角对齐。grid-overlay 图形作为你新的、移动后的坐标系的可视化表示。

添加一个红色和一个黄色方块:

. . .fill('#FF0000')square(0, 0, 100)fill('#FFFF00')square(100, 0, 100)

红色和黄色方块共享y参数为0,但黄色方块的 x 坐标为100。运行草图。Processing 根据新的原点相对定位两个方块。黄色方块应该出现在红色方块的右侧(见图 6-23)。

变换是累积的,这意味着每个后续的变换都会使用当前的坐标系作为参考,因此你可以通过使用额外的translate(100, 0)将黄色方块向右移动 100 像素:

. . .translate(100, 0)fill('#FFFF00')square(**0**, 0, 100)

新的translate()语句的x参数为100,而square()x参数现在为0。视觉效果应与图 6-23 相同。

f06023

图 6-23:水平相邻的红色和黄色方块

在第五章中,你学习了如何使用循环来排列 Truchet 图块。一个行和列变量追踪图块的位置。或者,你也可以使用translate(),每次循环迭代时移动坐标系。

rotate()

rotate() 函数围绕原点 (0, 0) 旋转坐标系统。它接受一个以弧度表示的单一参数。正值顺时针旋转,负值逆时针旋转。与所有变换函数一样,效果是累积的。此外,您可以自由地将 rotate() 与其他变换函数混合使用。

在第一个 translate() 函数下方添加一行新的 rotate() 语句,以旋转网格覆盖图形和红色与黄色方块:

. . .translate(150, 100)rotate(QUARTER_PI). . .

rotate() 函数使用 QUARTER_PI 弧度作为参数,相当于 45 度。请注意,QUARTER_PI 是一个预定义的 Processing 变量,相当于写作 PI/4

运行草图。两个方块应当看起来作为一个整体进行旋转,同时显示网格覆盖图形(图 6-24)。

f06024

图 6-24:旋转网格覆盖图形和两个方块

坐标系统围绕当前原点旋转,原点作为枢轴点。回想一下,这个原点通过 translate() 函数已经偏移了 150 像素的 x 和 100 像素的 y

函数的顺序非常重要。例如,交换 translate()rotate() 语句会产生不同的视觉效果。图 6-25 提供了对比。幽灵方块显示了先执行的变换结果。右边的图像是先执行 rotate() 时产生的结果,此时原点与显示窗口的左上角对齐。

f06025

图 6-25:translate()rotate() 函数的顺序很重要;左边的图像显示了先执行 translate(),右边的图像显示了先执行 rotate()

要绕方块的中心而不是其左上角旋转,将方块的中心与原点对齐,方法是调整 square() 函数的 xy 参数。

scale()

scale() 函数调整坐标系统的大小。一个参数会按比例缩放;两个参数分别控制 x 和 y 轴的缩放。scale(1)scale(1, 1) 不会产生任何效果,因为这些是默认的缩放值。

要减小缩放比例,请使用介于 0 和 1 之间的浮动值。缩小现有元素的大小:

. . .translate(150, 100)rotate(QUARTER_PI)scale(0.5). . .

scale(0.5) 会将元素的大小缩小到原来的一半。与 rotate() 一样,缩放是相对于当前坐标系的原点而言的。换句话说,(0, 0) 保持不变,所有内容朝这个点收缩(图 6-26)。

f06026

图 6-26:使用 scale(0.5) 将大小减半

任何大于 1 的值都会放大。例如,要将所有内容的大小加倍,请使用 scale(2)。要在某个轴上进行反射/翻转,请使用负值。例如,scale(-1, 1) 会将所有内容水平翻转,生成元素的镜像图像。

shearX() 和 shearY()

剪切形状会使其沿水平或垂直轴倾斜。结果是一个形变的形状,面积不变。一个典型的剪切例子是将矩形转换为具有倾斜边的平行四边形。

shearX()shearY()函数分别应用水平和垂直剪切。每个函数接受一个以弧度为单位的单一参数。

要对你的网格覆盖图形和两个方块应用垂直剪切,可以注释掉rotate()行,并使用shearY()函数应用 45 度垂直剪切:

. . .translate(150, 100)**#**rotate(QUARTER_PI)scale(0.5)shearY(QUARTER_PI). . .

rotate()函数被注释掉,以使剪切的方向更加直观。shearY()的参数是一个正数,因此剪切会按顺时针方向应用。图 6-27 对比了这些代码更改的结果(左图)与shearX()操作(右图)。

现在你已经知道如何组合变换函数;然而,你通常会希望将变换效果限制在某些元素的范围内。接下来,让我们看看如何在单个草图中使用多个坐标矩阵。

f06027

图 6-27: shearY(QUARTER_PI)(左)和shearX(QUARTER_PI)(右)

pushMatrix()和 popMatrix()

pushMatrix()popMatrix()函数允许你隔离任何变换函数的效果。通过这种方式,你可以对选定的元素执行不同的变换,这对于元素组特别有用。

你添加到草图中的任何元素都会相对于坐标系的原点定位。请记住,每个新的变换函数都会影响原点的位置或方向,并且每个新的变换都会受到前一个变换的影响。

如果你想将translate()scale()应用于黄色方块,但不想应用shearY(),可以将红色和黄色方块隔离开来,将每个方块放置在pushMatrix()popMatrix()内:

. . .1 translate(150, 100)#rotate(QUARTER_PI)scale(0.5)pushMatrix()2 shearY(QUARTER_PI)image(grido, 0, 0)fill('#FF0000')square(0, 0, 100)popMatrix()pushMatrix()3 translate(100, 0)image(grido, 0, 0)fill('#FFFF00')square(0, 0, 100)popMatrix()

pushMatrix()函数为shearY() 2 和translate() 3 创建了新的矩阵,这两个函数都扩展了上面translate(150, 100) 1。popMatrix()函数恢复了之前pushMatrix()行之前的坐标系。我添加了另一个网格覆盖图形,以帮助可视化两个坐标系之间的差异。

作为替代方法,你可以在红色方块后添加shearY(-QUARTER_PI)来撤销剪切,但推送和弹出矩阵是更优雅的解决方案。

运行草图。如图 6-29 所示,黄色方块应该被平移和缩放,但不被剪切。

f06029

图 6-29: 黄色方块已被平移和缩放,但未进行剪切。

现在假设你想将由多个形状组成的图形移动到显示窗口中。图 6-30 展示了一个鱼缸模拟,每条鱼由多个形状组成。每只眼睛(一个圆形)都有自己的 x-y 坐标,每个定义曲线或直线的顶点也是如此。

f06030

图 6-30:使用 pushMatrix()popMatrix() 进行形状组的平移

要追踪和更新所有这些 x-y 坐标,你必须将它们存储在全局变量中,以便在每一帧中递增。更高效的做法是将每条鱼定义在一对 pushMatrix()popMatrix() 函数中。这样,你可以通过使用一对全局 x-y 坐标和 translate() 函数来控制一条鱼的位置。

使用 pushMatrix()popMatrix() 函数,包含不同形状的组,每个组使用不同的变换函数序列。你可以添加动画效果。如果需要,重新使用 image(grido, 0, 0) 行,在每个 pushMatrix()... popMatrix() 栈中帮助你可视化正在发生的变化。

挑战 #6:模拟时钟

在这个挑战中,你将使用本章学到的所有技巧来创建一个显示当前时间的模拟时钟。时钟将每秒更新一次,因此你需要使用 draw()。为了旋转秒针、分针和时针,你将使用变换函数。

创建一个新的草图,并将其保存为 analog_clock。添加以下代码:

def setup(): size(600, 600) frameRate(1) noFill() stroke('#FFFFFF')def draw(): background('#004477')

帧率设置为 1,足以每秒更新一次秒针的位置。

要获取相关的时间值,请使用 Processing 的 hour()minute()second() 函数。每个函数与计算机时钟通信并返回一个整数值;这些函数不需要任何参数。在 draw 块中添加代码以在控制台显示当前时间:

 . . . h = hour() m = minute() s = second() print('{}:{}:{}'.format(h, m, s))

运行草图。在每一帧中,控制台会显示当前的小时(0 到 23)、分钟(0 到 59)和秒数(0 到 59),并用冒号分隔。时间应与系统时钟一致,通常显示在屏幕的角落。

创建一个数字式时钟(即没有指针,只有数字)的方式非常简单,只需结合时间和 text() 函数即可。然而,对于模拟时钟,你需要将小时、分钟和秒数转换为旋转角度。

从绘制表盘和时针开始:

 . . . 1 translate(width/2, height/2) strokeWeight(3) 2 circle(0, 0, 350) # hour hand 3 strokeWeight(10) line(0, 0, 1004, 0)

translate() 函数 1 将原点定位在显示窗口的中心。这样可以简化时钟指针的旋转,因为 rotate() 函数是围绕坐标系统的原点旋转的。circle() 函数将 x-y 参数都设置为零 2,因此它位于显示窗口的中心(图 6-31)。时针是最粗(也是最短)的,笔画粗细为 10 3,长度为 100 像素 4。

f06031

图 6-31:带时针的时钟表盘

目前时针指向 0 弧度(指向东方)。回想一下,在使用arc()函数绘制时,角度是从该点顺时针打开的(向南)。然而,如果时针从三点钟位置开始,你的时钟会偏移三个小时。你可以使用rotate()函数来校准:

 . . . rotate(-HALF_PI) # hour hand . . .

HALF_PI 等于 PI / 2;通过在前面加上 符号,你可以实现逆时针旋转。运行草图,时针现在应该指向十二点(直接向上)。

下一步是计算时针每小时前进多少弧度。考虑到一圈是 2π 弧度,因此一个小时等于 PI * 2 / 12。因此,六点钟是 PI * 2 / 12 * 6。不过,你可以使用 TAU 代替 PI * 2。例如,六点钟等于 TAU / 12 * 6

将时针旋转到当前时间:

 . . . # hour hand rotate(TAU / 12 * h) . . .

在十二点钟,时针指向正上方。这是因为 TAU / 12 * 12 等于 TAU,即完成一圈。对于其他每个小时,时针应该指向正确的位置(图 6-32)。当然,时针的角度将取决于现在是一天中的哪个时间。

现在添加分钟和秒针。最终的效果应该类似于图 6-33。

秒针应该每秒钟前进一次。比较控制台中的时间和视觉输出,确保代码正确运行。如果你需要帮助,可以访问解决方案:github.com/tabreturn/processing.py-book/tree/master/chapter-06-motion_and_transformation/analog_clock/

f06032

图 6-32:时针指向两点钟

f06033

图 6-33:完成的时钟

总结

在本章中,你学习了如何为动画结构化一个 Processing 草图。为了在不同的帧之间管理变量,你学会了如何使用全局变量。你可以在每帧增量更新全局变量,以控制形状坐标,从而实现流畅的动画效果。你还学会了如何将帧保存为图像。你可能会将动画保存为一系列图像,然后通过视频编辑软件将它们合成成电影。

你还看到了如何通过变换函数操作坐标系,允许你平移、旋转、缩放和剪切你的元素。你学习了如何修改坐标系,以便对特定组的元素应用变换。通过一次平移操作移动一组形状,比管理大量坐标变量要容易得多。此外,对单一形状(更别说是一个组)应用旋转、缩放和剪切操作,否则将涉及复杂的矩阵计算。

在下一章,你将学习关于 Python 列表的内容以及如何从外部文件读取数据。列表将为你提供强大的方法来管理和操作值,将它们视为元素集合,而非单独的个体。为了帮助可视化列表值,你还将探索数据可视化技巧。

第七章:操作列表和读取数据

当你需要处理多个值时,可以通过使用 Python 列表将它们分组到一个变量中。列表数据类型可以存储任意数量的项,并能高效、动态地管理和操作这些项。例如,你可以创建一个列表来存储你最喜欢的电影标题,并使用内建的方法插入新电影、重新排序排名或仅显示排名在 30 到 40 之间的标题。

在本章中,你将学习如何创建和操作列表,然后将其与循环结合,访问并对每个项执行操作。为了配合本书的视觉主题,你将生成列表数据的图形表示,包括显示亮度和 RGB 混合的颜色列表图表,以及展示史上畅销视频游戏的图表。你将看到如何调整列表值以影响视觉输出,并观察图表如何根据数据变化进行调整。

你还将学习如何从文本文件中读取数据,以及文本格式与其他文件格式的区别。你将把 Python 列表数据转储到 CSV 格式的文本文件中,并在草图运行时加载它,这样你就可以使用其他工具(如电子表格)准备数据。

引入列表

列表包含多个相关或属于同一组的值。例如,考虑编写一个视频游戏,在游戏中玩家四处游走,收集各种物品——钥匙、武器、盔甲升级等等——以便晋级到新的一关。你的游戏需要跟踪这些物品,这些物品可以存储在一个库存列表中。

要表示一个列表,可以使用方括号并用逗号分隔每个元素。例如,以下是一些游戏物品的简单列表:

inventory = ['key', 'gem', 'sword', 'apple', 'book']

这个列表包含五个字符串,并被分配给一个名为inventory的变量。

对物品集合执行重复操作是常见的编程挑战。假设你想显示一个包含玩家已收集物品的网格(图 7-1)。你可以编写一个loop语句来访问库存中的每个物品,并将其绘制在一个单元格中。如果列表的大小发生变化——因为玩家添加或丢弃了物品——循环会自动适应,因此你只需编写一次代码,程序就会填充适当数量的单元格来表示库存物品。

f07001

图 7-1:来自游戏Minetest的玩家库存

在 Python 中,单个列表可以包含任何混合的数据类型和重复值。例如,这个最高分条目存储了多种数据类型:

topscore = ['LEO', 54120] 

玩家名称LEO是一个字符串,而高分是一个整数。

列表可以包含任意数量的元素,甚至可以通过使用一对空方括号(中间没有任何内容)来定义一个空列表,这在你打算在程序运行时添加物品时非常有用。

列表是有序的,在许多情况下,顺序很重要——例如,在这个彩虹颜色的顺序中:

rainbow = [ 'red', 'orange', 'yellow', 'green', 'blue', 'indigo', 'violet',
]

在定义列表时,你可以将它跨越多行书写,如下所示,这样可以让代码更容易阅读和编辑。Python 还允许在最后一个元素后添加一个可选的尾随逗号。这个额外的逗号有助于你在需要添加或调整列表值时,不小心忘记加逗号的情况。

创建和访问列表

为了熟悉如何定义、访问和修改列表,创建一个新的草图。将其保存为 rainbow_list 并添加以下代码:

rainbow = ['blue', 'orange', 'yellow']print(rainbow)

现在,这个彩虹缺少了一些颜色,且顺序不正确,因此在你继续进行本节时,将使用各种列表操作来添加和调整颜色。首先,运行代码以验证以下控制台输出:

['blue', 'orange', 'yellow']

打印 rainbow 变量会显示所有三个值,并且带有方括号和逗号。

在许多情况下,你可能希望检索单个元素,而不是整个列表。要显示指定颜色,请在方括号中指定它的位置或 索引。请注意,Python 列表的索引从 0 开始,因此要打印第一个元素,输入以下内容:

. . .print(rainbow[0])

运行草图以确认控制台显示 blue

第二个元素 orange 的索引是 1,这个列表中的最后一个元素 yellow 的索引是 2。要打印项目 1 和 2,请输入以下内容(请注意,在本章中,print 语句旁边的注释会指出在控制台中应该出现的内容):

. . .print(rainbow[1]) # displays: orangeprint(rainbow[2]) # displays: yellow

这种语法可能让你想起第三章处理字符串时使用的切片符号,应该会因为它的工作方式相同而产生这种联想。就像使用切片符号一样,使用-1可以访问列表的最后一个元素,并通过使用冒号定义的范围来提取子集元素。试试以下代码:

. . .print(rainbow[-1]) # displays: yellowprint(rainbow[-2]) # displays: orangeprint(rainbow[0:2]) # displays: ['blue', 'orange']

如果你指定了超出列表范围的任何索引,比如 rainbow[3] 或更高,Processing 会显示 IndexError 错误信息。

修改列表

列表在行为上是动态的,可以在程序运行时发生变化。你可以用新值覆盖任何元素,并使用不同的列表方法插入新元素或删除现有元素。例如,在游戏物品栏中,如果玩家找到了更强大的武器,可以替换掉原有的武器,并随着玩家交易物品来添加或移除元素。

回到彩虹示例,你需要将蓝色替换为红色,使其成为 rainbow 列表中的第一个颜色。要修改现有的列表元素,可以像操作其他变量一样重新赋值,但在列表中,你需要使用方括号指定元素的索引。将以下行添加到 rainbow_list 草图的末尾:

. . .
rainbow[0] = 'red'

red 字符串现在替换了 blue,覆盖了它,成为列表中的第一个项目。打印 rainbow 列表应确认这一点:

print(rainbow) # ['red', 'orange', 'yellow']

蓝色不再出现在 rainbow 列表中。

让我们看一下几个最有用的列表方法,并附上可以添加到工作草图中的代码。每个示例都建立在之前的代码之上,因此按顺序逐一完成它们,在前进的过程中输入这些行。

append()方法

append()方法将一个元素添加到列表的末尾,无论列表的长度是多少。将蓝色添加到rainbow列表的末尾:

rainbow.append('blue')print(rainbow) # red, orange, yellow, blue

注意,在这些示例中,print()函数后的注释仅包含颜色的顺序;当你实际打印列表时,控制台将显示['red', ...,'blue'],包括所有的方括号和引号。

extend()方法

要将一个列表中的所有元素添加到另一个列表的末尾,可以使用extend()方法:

colors = ['indigo', 'violet']
rainbow.extend(colors)print(rainbow) # red, orange, yellow, blue, indigo, violet

包含靛蓝和紫罗兰色的colors列表现在被添加到原始的rainbow列表中。

index()方法

index()方法返回与提供的参数匹配的元素的索引(该元素在列表中的位置,作为整数)。如果有多个匹配项,该方法会返回第一个实例。使用'yellow'作为参数来测试这一点:

yellowindex = rainbow.index('yellow')print(yellowindex) # 2

尝试不同的颜色参数。如果没有匹配的值,Processing 会显示一个ValueError错误消息。

insert()方法

insert()方法接受两个参数:第一个是插入元素的索引;第二个是要插入的值。将green插入到列表的中间,索引参数为3

rainbow.insert(3, 'green')print(rainbow) # red, orange, yellow, green, blue, . . .

现在绿色位于原本蓝色的位置,将蓝色和它右边的所有颜色都向后移动了一个索引。

pop()方法

pop()方法接受一个参数:要移除的元素的索引。如果需要,可以使用被“弹出”的值进行其他操作。将靛蓝从列表中弹出并赋值给名为i的变量;然后打印irainbow,以确认控制台输出与这里的注释一致:

i = rainbow.pop(5)print(i) # indigoprint(rainbow) # red, orange, yellow, green, blue, violet

如果你不关心使用被弹出的值,可以去掉i =部分。

现在,使用没有参数的pop()方法移除列表中的最后一个元素:

i = rainbow.pop()print(rainbow) # red, orange, yellow, green, blue

控制台输出应该确认紫罗兰色已从列表中移除。

remove()方法

remove()方法移除第一个值与提供的参数匹配的元素。通过使用extend()方法重新添加靛蓝和紫罗兰色,然后使用remove()方法移除靛蓝:

rainbow.extend(colors)print(rainbow) # red, orange, yellow, green, blue, indigo, violet
rainbow.remove('indigo')print(rainbow) # red, orange, yellow, green, blue, violet

在扩展列表后,rainbow恢复为七色列表。在执行remove行后,列表又变成了六个颜色,没有靛蓝色。

Python 提供了其他列表方法,但这些方法足够让你开始操作列表。任何合适的 Python 参考资料或网络搜索应该能覆盖其余的内容。例如,如果你想重新排序列表元素,可以查找reverse()和按字母数字顺序的sort()方法。Processing 参考资料还包括几个列表方法,它们是标准的 Python(而非 Processing)特性,并且在任何 Python 环境中都可以使用。

结合循环和列表

你可以编程使用循环处理列表,这样可以节省无数的手动指令行。例如,假设你想创建一个Breakout风格的游戏(图 7-2)。在这种类型的游戏中,玩家控制屏幕底部的挡板,目标是将球弹向上方摧毁所有砖块。你可以创建一个列表来存储砖块,当玩家用球击中砖块时,该砖块将从列表中移除。在某些关卡中,你可能希望在游戏过程中出现额外的砖块,这意味着你需要插入新的列表元素。

f07002

图 7-2:LBreakout2,一个开源的Breakout克隆游戏

你可能已经玩过这种游戏的变种,并且可能知道,在砖块被摧毁时,某些砖块会掉落能量道具。你还知道砖块有不同的颜色,有些可能是不可见但坚固的,而有些可能需要多次击打才能摧毁。你可以通过使用列表的列表来编程这些附加属性。列表可以包含其他列表,而这些列表又可以包含进一步嵌套的列表(请参见第 144 页的“创建列表的列表”)。

如果你的列表名为bricks,并包含 60 个砖块的填充色,那么渲染每个砖块至少需要和元素个数一样多的代码行。例如,你可以使用以下代码使用rect()函数绘制每个砖块:

bricks = [ '#FF0000',  '#FF0000'`,` . . .# brick A1fill(bricks[0])rect(0, 0, 30, 10)# brick A2fill(bricks[1])rect(30, 0, 30, 10). . .# brick F10fill(bricks[59])rect(270, 50, 30, 10)

请注意,每个渲染的砖块都需要fill()rect()函数。即使你删除了注释,这也意味着要绘制完整的列表,你需要 120 行(60 × 2)的代码。这并不高效,也无法处理可能长度变化的列表。

使用颜色值列表绘制图形

对于这个练习,你将从一个十六进制值的列表中绘制一条彩虹色带,首先使用fill()rect()函数绘制一条单一的色带。然后,你将调整代码,使用循环绘制整个列表。首先,将以下代码添加到你的rainbow_list草图中:

. . .size(500, 500)noStroke()background('#004477')
bands = [ '#FF0000', # red '#FF9900', # orange '#FFFF00', # yellow '#00FF00', # green '#0099FF', # blue '#6633FF' # violet
]# red bandtranslate(0, 100)fill(bands[0])rect(0, 0, width, 50)

到目前为止,草图完全依赖于控制台输出。此代码首先定义了显示窗口的大小,关闭了描边,并设置了背景颜色。bands列表包含六种颜色的十六进制值,并附有注释以标识每种颜色值。第一条(红色)色带使用translate()fill()rect()函数绘制。运行草图,结果应该是在蓝色背景上绘制一条单一的横向红色色带(图 7-3)。

f07003

图 7-3:运行草图的结果是一条单一的红色色带。

你已经绘制了列表中的第一条色带,下一步是调整代码,使用for循环来绘制所有六条色带。

当你将 for 循环与列表结合时,Python 会将每个连续的列表值赋给循环变量,并使用列表的长度来决定所需的迭代次数。为了让你的程序绘制 bands 列表中的每个彩带,注释掉现有的 fill()rect() 函数,然后添加一个循环来为你绘制完整的彩虹:

. . .**#**fill(bands[0])**#**rect(0, 0, width, 50)for band1 in bands: fill(band) rect(0, 0, width, 50) translate(0, 502)

在这种情况下,如果你将循环变量命名为 band 而不是像 i 这样的名称,代码会更易于理解。第一次迭代时,band 变量等于 '#FF0000',第二次是 '#FF9900',依此类推。translate() 函数将坐标系沿带的高度移动。每次迭代时,Processing 会应用列表中的下一个填充色,并在上一个矩形下方绘制一个新矩形。结果是在显示窗口的宽度上堆叠六个彩虹色的带子(见图 7-4)。请注意,绿色带子在电脑屏幕上会比在印刷书籍中的显示更亮。标准印刷墨水(青色、品红、黄色和黑色—CMYK)无法复制数字显示器上绿色的色调强度。

f07004

图 7-4:六种彩带的彩虹序列

在这个例子中,Python 会检索列表中的每个元素,因此你无需指定任何索引值。在下一节中,你将使用 enumerate() 函数来获取每个元素的索引和值。

使用 enumerate() 进行循环

对于某些循环任务,你需要每个元素的索引和值。例如,假设你有一个你最喜欢的电影的有序列表,并且想要打印每个电影标题及其排名(索引)。你可以使用 enumerate() 函数来实现。

要使用 enumerate() 函数获取彩虹中每个彩带的索引,在 forin 之间提供两个变量名。这两个变量将分别存储索引和值,供每次迭代使用。修改你在 rainbow_list 示例中的代码:

. . .**#**for band in bands:1 **for i, band in enumerate(bands):** fill(band) rect(0, 0, width, 50) 2 **fill('#FFFFFF')** **textSize(25)** **text(i, 20, 35)** translate(0, 50)

iband 变量分别表示索引和值。额外的 fill 和下面的两行 text 会在每个矩形上绘制索引数字。

运行示例代码。现在你应该能在每个彩带中看到一个白色数字(见图 7-5),尽管 2 在黄色上不是特别明显。

f07005

图 7-5:带编号的彩虹带序列

在需要操作列表索引或跟踪循环迭代次数时,使用 enumerate() 函数。如果是其他对列表的循环操作,普通的 for 循环就足够了。

创建列表的列表

尽管列表内嵌列表的概念看起来可能很复杂,但适当嵌套的列表使复杂的数据集更容易管理。在这个实际的数据可视化任务中,你将创建一个条形图的变体。这个图表将测量六种颜色的相对亮度。图 7-6 显示了你正在努力达到的简化表示。注意,黄色是最亮的颜色,因此它的条形最长。

f07006

图 7-6:条形图的简化轮廓表示

最终的图表将包括颜色,条形会进一步分为红色、绿色和蓝色的几个部分,以表示每种颜色的 RGB 混合(稍后会详细介绍)。

创建条形图的第一步是启动一个新的草图并将其保存为 lists_of_lists。添加以下设置代码:

size(500, 380)background('#004477')noFill()stroke('#FFFFFF')strokeWeight(3)
h = 50translate(100, 40)
bands = 6rect(0, 0, 40, h*bands)

h 变量定义了条形的高度,translate() 函数定义了左上角的位置。视觉效果应显示为垂直条形;这表示六个带状图(图 7-7)。条形的高度表示一个整数值:6。如果 bands 等于 7,则定义条形的矩形将延伸超出显示窗口的底部。

f07007

图 7-7:一个 6 × 50 像素高的条形

下一步是将现有的条形分成六个部分,这些部分稍后将形成水平条形。在草图的末尾添加一个新的 bands1 彩虹颜色列表,并加入一个循环,使用每种颜色绘制一个矩形:

. . .
bands1 = [ '#FF0000', '#FF9900', '#FFFF00', '#00FF00', '#0099FF', '#6633FF'
]for band in bands1: fill(band) rect(0, 0, 40, h) translate(0, h)

这个 bands1 列表包含六个十六进制颜色值。这些值定义了每个部分的填充颜色。for 循环以列的方式绘制彩虹色的部分,覆盖了第一个条形(图 7-8)。

f07008

图 7-8:彩虹色矩形覆盖在原始条形上

下一步是将每个颜色块向右延伸,形成水平条形。每个条形的宽度将由其各自颜色的亮度决定。计算亮度的方法是将构成任何颜色的红色、绿色和蓝色值相加。例如,考虑白色。它是屏幕上最亮的“颜色”;在十六进制中表示为#FFFFFF,如果转换为百分比表示,则为 100%红色、100%绿色和 100%蓝色。总体亮度为 300%,或者如果你想取平均值,则为 300 ÷ 3 = 100%的亮度。

为了按 RGB 百分比管理颜色,你需要为每个 R/G/B 主色提供一个整数值,而不是一个十六进制字符串值。将一个新的 bands2 列表添加到代码的末尾,每个元素包含一个包含三整数的列表,表示每种颜色的红色/绿色/蓝色混合:

. . .
bands2 = [ [100, 0, 0], [100, 60, 0], [100, 100, 0], [0, 100, 0], [0, 60, 100], [40, 20, 100]
]

要直接访问另一个列表元素中的列表元素,请包含第二对方括号。例如,要检索第二个(橙色)条带中的绿色百分比,请输入以下内容:

print(bands2[1][1]) # 60

在这种情况下,绿色值为 60,你可以在控制台中确认这一点。

为了处理bands2列表中的百分比,请将colorMode()设置为使用 0 到 100 之间的 RGB 值。为了绘制条形图,重置并转换坐标系统,然后添加一个循环来绘制填充不同灰度的矩形:

. . .colorMode(RGB, 100)resetMatrix()translate(100, 40)for band1 in bands2: r = band[0] g = band[1] b = band[2] 2 sum = r + g + b 3 avg = sum / 3 4 fill(avg, avg, avg) rect(0, 0, sum5, h) translate(0, h)

每次迭代时,band被赋予 RGB 百分比值列表。然后,这些值被相加、计算平均值以得到亮度值,条形图的填充色被设置为基于该平均值的红、绿、蓝等量的灰度色。亮度值还决定了条形图的宽度。运行草图查看结果(图 7-9)。

奇怪的是,绿色条形图(从上数第四个)被指示为与红色(顶部)条形图亮度相当。还记得绿色在你的屏幕上比在打印上更亮。数学计算是正确的,但人眼对绿色光的敏感度更高,因为我们拥有更多的绿色光感受器,因此绿色条形图看起来更亮。可以通过数学方式来补偿这一点。如果你想测试,可以使用以下值乘以rgb变量:

 . . . r = band[0] *** 0.64** g = band[1] *** 2.15** b = band[2] *** 0.22** . . .

f07009

图 7-9:每个条形图的宽度代表每种颜色的相对亮度。

现在,黄色条(从上数第三个)是唯一一个比绿色条更宽/更亮的条形图。然而,对于这个任务,我想使用平均公式,因此请移除任何乘数,恢复为平均值。

接下来,调整现有的循环,使得每个条形图显示构成其填充的主要颜色的不同数量:

 . . . r = band[0] g = band[1] b = band[2] **#**sum = r + g + b **#**avg = sum / 3 **#**fill(avg, avg, avg) **#**rect(0, 0, sum, h) 1 fill('#FF0000') rect(0, 0, r, h) 2 fill('#00FF00') rect(r, 0, g, h) 3 fill('#0099FF') rect(r+g, 0, b, h) translate(0, h)

rect()函数形成包含最多三个段的水平条形图。每个段的大小和填充由颜色条带包含的红色、绿色和蓝色量决定。即使colorMode()设置为RGB,Processing 仍然可以将带引号的填充参数解释为十六进制值。

运行草图查看结果(图 7-10)。红色条形图位于顶部,完全由红色混合而成。紫色条形图位于底部,主要是蓝色,但也包含一些红色和一点绿色。

f07010

图 7-10:每个条形图显示其 RGB 主色的比例。

如果你向别人展示图表,他们可能完全不知道每个条形图代表什么颜色,因此添加标签将有助于阐明问题。为每个条带添加一个标签元素:

. . .bands2 = [ [100, 0, 0, **'red'**], [100, 60, 0, **'orange'**], [100, 100, 0, **'yellow'**], [0, 100, 0, **'green'**], [0, 60, 100, **'blue'**], [40, 20, 100, **'violet'**]]. . .

然后,在循环中添加一些行来绘制每个标签:

. . .for band in bands2: . . . fill('#FFFFFF') textAlign(RIGHT) text(band[3], -20, 30) translate(0, h)

这将文本填充设置为白色,右对齐,并在条形图旁边写下颜色标签。运行代码查看结果(图 7-11)。

f07011

图 7-11:带标签的完整图表

许多列表只需一维即可正常工作,例如购物清单。你可以将二维列表看作是网格或表格,这使得它们对于绘制二维图形非常有用。三维及其他高维列表也有其用途,但在使用这种结构之前,请考虑是否将二维列表添加另一个位置会更加合理。

挑战 #7:Breakout 关卡

在这个挑战中,你将重新创建一个Breakout关卡。设置代码将包括一个三维列表。处理这样的列表需要使用嵌套循环——也就是一个循环嵌套在另一个循环内。

结果应该类似于图 7-12。请注意,你并没有创建一个可以玩耍的游戏,带有有效的输入;它更像是在游戏进行中抓取的一张截图。

创建一个新草图,并将其保存为breakout_level。添加以下代码来绘制球和挡板:

size(600, 600)noStroke()background('#000000')# ball and paddlefill('#FFFFFF')circle(350, 440, 18)rect(300, 520, 190, 40)

f07012

图 7-12:完成的Breakout任务

这段代码应该会渲染出一个空的黑色舞台,白色的球和挡板,但还没有砖块。

现在,添加砖块的数据。为了节省时间,复制并粘贴我在 GitHub 仓库中的代码:

  1. 打开浏览器,访问github.com/tabreturn/processing.py-book/.

  2. 导航到chapter-07-working_with_lists_and_reading_data

  3. 定位并打开bricks.txt文件。

  4. bricks.txt的内容复制并粘贴到你的草图中。

如果你更愿意手动输入,以下是代码:

r = '#FF0000' # red
o = '#FF9900' # orange
y = '#FFFF00' # yellow
g = '#00FF00' # green
b = '#0099FF' # blue
p = '#6633FF' # violet
bricks = [ # col 0  col 1  col 2  col 3 [ [r,1], [o,1], [y,1], [g,1] ], # row 0 [ [o,1], [y,1], [g,1], [b,1] ], # row 1 [ [y,1], [g,1], [b,1], [p,1] ], # row 2 [ [g,1], [b,2], [p,2], [b,1] ], # row 3 [ [b,1], [p,2], [   ], [g,1] ], # row 4 [ [p,1], [   ], [   ], [y,1] ], # row 5 [ [   ], [   ], [   ], [o,1] ], # row 6 [ [g,1], [   ], [   ], [   ] ] # row 7
]

为了让这更易读,我以一种反映每个砖块视觉位置的方式输入了bricks列表。按照以下顺序,每个砖块都有一个填充颜色和击中计数(表示摧毁它所需的击中次数)。我用空列表表示每个缺失的砖块。

以第一个砖块为例:[r,1]。这个砖块的填充颜色是红色,需要一次(剩余的)击中才能摧毁。你可以从包含该砖块的列表中推断出列和行的位置;在这种情况下,它位于第 0 行,第 0 列。添加两个print()语句来确认这些信息:

. . .1 print(bricks[0]) # displays row 0 items2 print(bricks[0][0]) # displays the very first brick

这些print语句显示了bricks中的第一个元素,它是构成第 0 行第 1 列的四个砖块中的第一个,以及第 0 行第 2 列的第一个砖块。如果你想检索第一个砖块的颜色,可以输入以下内容:

print(bricks[0][0][0]) # displays #FF0000

请注意,颜色变量r包含一个十六进制值,因此在控制台中显示的是红色的十六进制值。

如我之前所提到的,你需要为此任务使用嵌套循环。以下几行代码将帮助你开始:

. . .
bw = width / 4
bh = height / 15translate(0, bh)for row in bricks: for col, brick in enumerate(row): if len(brick): # code to draw a brick x = col * bw

bw 变量定义了基于将四列排列到显示窗口中的砖块宽度;bh 计算砖块高度。外部的 for 循环遍历行,内部的 for 循环遍历每一行中的砖块。colbrick 变量分别表示列号和砖块。你可以使用 len() 函数来判断这个砖块是否是占位符(即一个空列表)。长度为 0 的 brick 相当于 False,此时 Python 会跳过 x = col * bw 这一行。x 变量将保存绘制每个砖块的 x 坐标。完成任务,以匹配 图 7-12 中显示的结果。请注意,大致位于中心的砖块的命中次数为 2,并且必须包括光泽效果。如果需要帮助,可以访问 github.com/tabreturn/processing.py-book/tree/master/chapter-07-working_with_lists_and_reading_data/breakout_level/ 获取代码。

在接下来的部分,你将学习如何处理来自外部文件的数据,并且你将使用列表技巧结合 Processing 函数来读取文本文件的内容。

读取数据

Python——以及 Processing——能够处理多种类型的文件数据。例如,你可以使用 Processing 创建一个包含各种音频和视频文件的游戏,并将这些多媒体资源存储在 data 子文件夹中。你之前在章节中已经从 PNG 文件加载了图像数据到 Processing 草图中;这一部分将重点讲解如何加载存储在基于文本文件中的数据。

你也曾处理过存储在列表中的值,但使用 Python 的列表语法将来自其他源的数据重新输入可能会很繁琐,尤其是对于大规模和可交换的数据集。一个替代方案是通过使用像电子表格这样的工具,在 Processing 之外管理和准备数据,将其保存为基于文本的格式,然后在运行草图时读取文件内容。为了理解什么是基于文本的文件与其他文件的区别,以及如何使用它们来存储数据,我们先简要介绍一下文件格式。

文件格式

文件格式 是一种标准化的编码信息的方式,用于在数字介质上存储数据。有许多不同的格式,每种格式的解读方式都不同。例如,应用程序以 可执行格式 编码,比如 Android 的 APK 文件或 Windows 的 EXE 文件。一些 多媒体格式 包括用于音乐的 MPEG-1/2 音频层 III(MP3)或用于图像的 JPG。

你可以通过文件扩展名来识别文件的格式。文件扩展名通常由三个字母组成,前面总是有一个点,附加在文件名的末尾。为了简化用户操作,许多操作系统隐藏文件扩展名,但如果你在 Windows 文件资源管理器或 Mac Finder 设置中深入查找,你可以让文件管理器显示扩展名。你的系统依赖这些文件扩展名来用合适的应用程序打开文件,并显示图标或生成缩略图(见图 7-13)。

f07013

图 7-13:显示文件扩展名的图标视图中的文件管理器

当你删除或重命名文件扩展名时,这种关联会丢失。或许你曾尝试在文本编辑器中打开 MP3 文件,结果看到一堆乱码,像这样:

. . .
���:����zc��E9���yoO��F�;#C��@##�&�#�##HV�D��#���X���#�&2XNf�##M�#�#���#J��,8,#`}##�#�4R�f�#E��V���d@��P������G��rjS#gbx�:P+�A��'��Q�IF��5�0�i.�A���sG�P"����oA~�#. . .

文本编辑器是为编辑文本编码文件而设计的;因此,它们会尝试将音频数据解释为字符。尽管你可能能在其中找到一些可理解的元数据,但 99%的内容是乱码。如果你使用 iTunes、Windows Media Player 或 VLC 打开这个文件,你会听到音乐。

有些文件格式是基于文本的,这意味着你可以在任何文本或代码编辑器中打开它们,并理解其内容。为了澄清,所谓的基于文本,是指纯文本,而不是包含不同颜色和大小字体的加粗或斜体的 Microsoft Word 文档。你可能会好奇为什么人们会使用纯文本,但它适用于简单的待办事项清单和编写几乎所有编程语言的代码,包括 Python。例如,Processing 文件是纯文本格式,尽管它有一个 .pyde 扩展名。

CSV

逗号分隔值(CSV) 文件,具有 .csv 扩展名,提供了一种简单的纯文本数据格式化方式。你将下载一个 CSV 文件,包含 Pink Floyd 专辑 The Dark Side of the Moon 的曲目列表。

每一行 CSV 文件代表一个条目,每个条目由一个或多个以逗号分隔的字段组成。以下是 The Dark Side of the Moon 的简化曲目列表,采用 CSV 格式:

location,title,creator,album,trackNum
file:///music/SpeakToMe.mp3,Speak to Me,Pink Floyd,The Dark Side of the Moon,1
file:///music/Breathe.mp3,Breathe,Pink Floyd,The Dark Side of the Moon,2. . .

该文件的第一行包含字段标题,接下来的行提供每个轨道的详细信息。你的电子表格软件(如 Microsoft Excel、LibreOffice Calc 或类似软件)会将 .csv 扩展名的文件与自己关联。打开任何 CSV 文件时,电子表格软件会以典型的行列方式显示信息(见图 7-14)。这种格式对于准备 CSV 数据很有用,但需要注意的是,一旦你保存为 CSV 格式,所有样式(如单元格大小、字体颜色等)都不会被保留。

f07014

图 7-14:在 LibreOffice Calc 中打开的完整 playlist.csv 文件

你现在将编写代码,从 CSV 文件加载曲目列表数据。创建一个名为 csv 的新草图,并创建一个 data 子文件夹,然后完成以下步骤:

  1. 打开你的浏览器并访问 github.com/tabreturn/processing.py-book/

  2. 转到 chapter-07-working_with_lists_and_reading_data

  3. 下载 data.zip 文件。

  4. 解压 ZIP 文件,并将 playlist.csv 移动到草图 data 子文件夹中。

Processing 提供了 loadStrings() 函数来读取基于文本的文件。它接受一个单一的参数(路径),指向你的文本文件,并将内容作为一个字符串列表返回,每个元素表示一行文本。添加以下代码来测试该函数:

csv = loadStrings('playlist.csv')for entry in csv: print(entry)

playlist.csv 数据被赋值给一个名为 csv 的列表。每个 csv 元素包含一行文本,表示单个轨道。for 循环在控制台中打印每个条目,每个条目占一行:

location,title,creator,album,trackNum
file:///music/SpeakToMe.mp3,Speak to Me,Pink Floyd,The Dark Side of the Moon,1
file:///music/Breathe.mp3,Breathe,Pink Floyd,The Dark Side of the Moon,2. . .

loadStrings() 函数无法区分不同的纯文本格式;这可以是一本畅销小说,也可以是最新的股市数据。

为了解释 CSV 数据,使用 split() 方法将每一行拆分成进一步的列表。在这种情况下,你将拆分每个条目,以便提取每个轨道的编号和标题;你不需要文件位置、创作者或专辑信息。split() 方法通过使用你选择的分隔符来工作。在这个例子中,你将使用逗号。修改你的 for 循环代码如下:

. . .1 for entry in csv[1:]: 2 track = entry.split(',') print('{}. {}'.format(track[4], track[1]))

通过添加 [1:]for 循环跳过 csv 列表中的第一个项目 1,以避免打印字段标题。每次迭代时,split() 方法将一个新的列表分配给 track 变量 2。元素 tracks[4]track[1] 分别保存了轨道编号和标题。

运行草图以确认控制台显示一个包含 10 个编号轨道的列表:

1\. Speak to Me
2\. Breathe. . .

如果你想向文件中写入文本,请查阅在线 Processing 文档中的 saveStrings() 函数;它实际上是 loadStrings() 的反向操作。

在 CSV 文件中格式化纯文本数据是避免在 Processing 编辑器中管理数据的好方法。CSV 的美在于其简单性,但它不适合处理层次结构化数据。在第八章中,你将学习其他基于文本的格式(如 XML 和 JSON)。

挑战 #8:游戏销售图表

在这个最终挑战中,你将生成一个所有时代畅销视频游戏的条形图。图 7-15 展示了最终结果(左侧)以及放大版(右侧)以提供更多细节。

f07015

图 7-15:完成的图表(左侧)和图表细节(右侧)

数据来源于维基百科的一篇文章《最畅销视频游戏列表》(en.wikipedia.org/wiki/List_of_best-selling_video_games),并将其从 HTML 表格转换为制表符分隔文件。自本书出版以来,排名可能已经发生变化,但这对于本练习的目的并不重要。

你将通过使用loadStrings()函数读取销售数据,然后使用本章学到的技巧绘制图表。创建一个名为game_sales_chart的新草图,并添加一个data子文件夹。在前面的练习中,你下载了一个data.zip文件,其中也包含了一个list_of_best-selling_video_games.tsv文件;将其放入草图的data子文件夹中。

这个文件使用制表符分隔的值,因此文件扩展名为.tsv。我使用了制表符,因为游戏标题或工作室/发行商名称包含制表符的可能性非常小,但可能会有逗号,使用split(',')方法可能会受到干扰。你可能想在你喜欢的电子表格应用程序中打开 TSV 文件,查看其中的值。总共有 50 款游戏,按销量从高到低排序。如果你用文本编辑器打开文件,你应该会看到类似下面的内容:

Rank	Title	Sales	Developer(s)	Publisher(s)
1	Minecraft	180000000	Mojang	Xbox Game Studios
2	Tetris	170000000	Elektronorgtechnica	Various. . .

一个单一的、不可见的制表符字符分隔每个字段。请注意,制表符的大小在不同编辑器之间可能有所不同,因此不一定会形成对齐的列,所以文件的显示可能会有所不同,具体取决于你使用的编辑器。

向你的草图中添加基本的设置代码,定义显示窗口的大小和背景颜色,并读取 TSV 数据:

size(800, 800)background('#004477')
tsv = loadStrings('list_of_best-selling_video_games.tsv')noStroke()

一个名为tsv的列表保存了游戏销售数据。没有图形元素的轮廓,所以我加入了一行noStroke()

你需要进行计算,将条形图的大小缩放到适合显示窗口。虽然销售数字看起来像是数字,但 Processing 将其视为文本。回想一下,字符串数据不能进行数学运算。幸运的是,有一个简单的解决方法。int()float()函数可以分别将不同的数据类型转换为整数和浮点数值。以下是一个示例:

entry1 = tsv[1].split('\t') # Minecraft entry
sales1 = entry1[2] # 180000000print(int(sales1) + 1) # 180000001

split()方法必须使用制表符字符作为分隔符,从第一个条目(Minecraft)创建一个列表;要指定制表符,使用'\t'作为参数。变量sales1等于索引 2 处的值,即销售列。尽管看起来像是数字,但这个值是一个字符串,因此print语句使用int()函数将sales1转换为整数,然后加 1。

现在,按照图 7-15 中的示意图完成图表。最好从一个打印每个条目的循环开始。接着,在创建条形图之前先显示标签。一旦有了标签,创建正确宽度的纯白色条形,然后用彩虹顺序效果来完成。如果需要帮助,可以访问github.com/tabreturn/processing.py-book/tree/master/chapter-07-working_with_lists_and_reading_data/game_sales_chart/获取解决方案。

总结

在这一章中,你学习了 Python 提供的一系列用于执行各种列表操作的方法,如何使用列表管理项目集合,以及在与循环结合使用时,列表的强大功能。你还学会了如何利用嵌套列表来管理更复杂的数据,并练习了一些数据可视化技巧。

此外,你已经学习了如何处理存储在纯文本格式中的数据,如 CSV 和 TSV,这使得在运行草图时能够从外部文件读取数值。这意味着你不需要在 Processing 编辑器中管理数值,从而使得更换数据集变得更加容易。

下一章将介绍字典,它们与列表类似,都用于存储项目集合。不过,字典通过使用词语而非索引来访问数值。你将再次利用新技能创建独特的数据可视化。

第八章:字典和 JSON

字典保存着一组项目,类似于你在第七章学习过的有序列表。不过,字典是无序的,你使用关联的值来访问每个项目,这使得你更容易记住字典中项目所代表的内容。在本章中,你将学习 Python 的字典语法和方法,如何结合循环和字典,以及如何嵌套字典和列表。

你还将学习如何使用另一种纯文本文件格式:JavaScript 对象表示法(JSON)。它的语法不像 CSV 那么简单,但更适合处理复杂的数据结构。你将使用 Python 内置的json模块来从 JSON 文件中读取字典数据,并且像第七章一样,你将创建一个数据可视化。

引入字典

在字典中,每个无序项目与一个称为的值关联。键通常是一个短字符串,每个字典项目由一个键值对组成。这意味着字典是关联的,一些编程语言将字典类型的结构称为关联数组。字典与列表不同,后者是按数字索引的,因为列表中的每个元素对应一个数字(索引),表示它在项目序列中的位置。

作为键值对如何工作的一个示例,你可以使用字典记录每个朋友的最喜欢的电影。在这样的字典中,每个键将是朋友的名字,每个值将是对应的电影标题。要插入或检索你朋友 Lee 的最喜欢的电影,你需要使用键'Lee'。图 8-1 是这个字典的概念图。

f08001

图 8-1:一个字典的示意图,表示键和值之间的映射关系

在你进行 Python 字典练习的第一个任务中,你将编写代码来管理学生记录。创建一个名为dictionaries的新草图,并添加以下代码,展示列表和字典之间的区别:

1 student = ['Sam', 24]2 student = {'name': 'Sam', 'age': 24}

首先,注意字典使用大括号({}),而列表使用方括号([])。列表 1 和字典 2 的变体存储的是相同的值:'Sam'24。然而,每个字典项目包含一个值一个键。在这个例子中,字典的键是'name''age'。记住,合理命名的键有助于识别值所代表的含义。这个student字典包含两个键值对(图 8-2)。

你将使用有意义的键来从student字典中检索值。要使用student列表,你需要记住每个值似乎是随意的位置。列表更适合处理有序的项目序列,但如果你有一组唯一的键与值映射,最好使用字典。

f08002

图 8-2:一个包含两个键值对的字典

字典可以存储各种数据类型,包括字符串、数字、布尔值、列表,甚至其他字典。您可以在字典中存储任意数量的键值对。从技术上讲,字典是有上限的,但如果您管理如此大量的数据,可能应该考虑使用数据库解决方案。

访问字典

要访问任何字典的值,请使用字典名称和关联的键,键用方括号括起来。试试以下代码:

. . .print(student['age']) # displays: 24print(student['name']) # displays: Sam

每个print()函数旁边的注释会确认控制台中应该显示的内容。

要打印整个字典,省略方括号和键,只保留字典名称:

print(student) # {'name': 'Sam', 'age': 24}

控制台应显示每个键值对,连同花括号、冒号和逗号。

键值对的顺序不一定与您定义它们的顺序一致,这在不同的 Python 环境中可能有所不同。字典本质上是无序的;Python 关注的是键与值之间的关联。如果您需要按键或值对字典项进行排序,可以使用各种函数和方法。您将在第 163 页的“结合循环和字典”中看到一些排序技巧。

如果您尝试引用一个不存在的键——比如student['grade']——处理时会显示KeyError消息。如果您需要检查一个键是否存在,请使用in操作符:

. . .if 'age' in student: print(student['age'])

in操作符检查'age'键是否存在于student字典中。如果找到,操作返回True,并且if语句执行print行,显示age值(在此例中为24)。

修改字典

字典是动态结构,因此您可以随意添加和修改键值对。要更改现有值,像处理列表元素一样重新赋值,但使用键而不是数字索引。举个例子,在您的字典示意图中,将学生的年龄改为25

. . .
student['age'] = 25print(student) # {'name': 'Sam', 'age': 25}

控制台输出应该确认年龄已从24更改为25

要添加新的键值对,请按照相同的过程进行。向student字典中添加学生 ID 号码:

. . .
student['id'] = 199505011print(student)# {'name': 'Sam', 'id': 19950501, 'age': 25}

这里的id值表示出生日期(1995-05-01)1。系统可以利用此信息计算学生的年龄,因此现在不再需要存储个人的age值。要删除它,请使用del语句:

. . .del student['age']print(student) # {'name': 'Sam', 'id': 19950501}

del语句永久删除age键及其对应的值。

嵌套字典和列表

字典可以包含其他字典或列表,列表也可以包含字典。让我们来看两个例子:一个字典包含列表,另一个列表包含字典。两者都是有效的数据结构方式,但正如您所见,您将根据哪种方式在特定应用中效果最好来选择。

此时,你的程序只存储了一个学生的详细信息。一个只管理一个学生的系统并不太实用,因此为了处理多个学生,试试 字典的列表。将以下代码添加到你 字典 草图的底部:

. . .
students = { 1 'names': ['Sam', 'Lee'], 2 'ids': [19950501, 19991114]
}print(students['names'][1]3) # Lee

names 列表项包含两个名字 1;ids 列表包含它们各自的 ID 号码 2。要直接访问字典项中的任何列表元素,使用关联的键后跟一对方括号,其中包含元素的索引 3。

另一种构建数据的方式是使用 字典的列表。与其将学生姓名分到一个列表,将 ID 分到另一个列表,不如为每个学生使用一个字典。将以下代码添加到草图的底部,实际上会完全覆盖原来的 students 字典:

. . .
students = [ {'name': 'Sam', 'id': 19950501}, {'name': 'Lee', 'id': 19991114}
]print(students[1]['name']1) # Lee

通过使用元素索引,接着是另一对方括号,包含字典键 1,来检索名字 Lee

在这两种方法中(字典列表)的后者可以说是更合理的结构。每个项就像是电子表格中的一行,包含单个学生的详细信息,而每个学生可能有不同数量的列。这意味着,你可以为 Sam 的字典添加一个额外的键值对,而无需为 Lee 做同样的事情。然而,使用第一种方法时,这将变得有些棘手。

你为键命名的方式以及如何选择嵌套列表和字典,应该有助于将数据与现实世界的模型关联,同时减少复杂性。保持键名简短且具有描述性,并记住,结构良好的数据将为你程序中后续的算法提供更直观的理解。换句话说,如果数据结构直观有序,编写代码时你将节省时间和精力。

循环和字典的结合

你经常需要遍历字典。例如,你可以通过使用单个循环填充预定义模板,动态生成系统中每个学生的报告。考虑到字典可以包含成千上万甚至百万个键值对,这是一种强大的技术。然而,由于键值系统的存在,迭代字典与迭代列表略有不同。

你可以使用 Python 的 keys()values()items() 方法迭代字典的键、值或键值对。请注意,许多列表方法——如 append()extend()index()insert()remove()——无法应用于字典数据类型。

首先,添加一个名为 courses 的新字典到你 字典 草图的末尾:

. . .
courses = { 'game development': 'Prof. Smith', 'web design': 'Prof. Ncube', 'code art': 'Prof. Sato'
}

字典的键代表课程标题;关联的值是协调每门课程的教授。接下来,你将看到如何将这个字典与 for 循环结合起来。

键的迭代

你可以编写一个仅处理键的for循环,如果你不需要处理字典中的值,这会非常有用。在使用字典的for...in语句时,键的迭代是隐式发生的。可以在下面的示例中测试这一行为,它应该在控制台中显示所有课程标题:

. . .for course in courses: print(course)

在每次迭代中,Python 会将courses中的下一个键分配给courseprint语句将每个课程标题显示在控制台的独立一行中,循环在所有键都被迭代后结束:

web design
game development
code art

请记住,你不能依赖字典项的顺序。如果你想确保按字母数字顺序获取键,可以将sorted()函数应用于courses

. . .for course in **sorted(**courses**)**: print(course)

修改后的for语句按以下顺序打印键:

code art
game development
web design

如果你只需要键的列表,可以使用keys()方法,若要对其进行排序,加入sorted()函数:

print(sorted(courses.keys()))# displays: ['code art', 'game development', 'web design']

这会在控制台打印一个包含键的列表,括号和逗号也会显示出来。

迭代值

values()方法返回字典的所有值,如果你不需要处理字典中的键,这非常有用。向示例中添加一个新循环,使用values()方法来获取每个教授的名字:

. . .for prof in courses.values(): print(prof)

在每次迭代中,Python 会将courses中的下一个值分配给prof。我将这个变量命名为prof,是教授(表示它将包含的值)的缩写。print语句将每门课程的教授名称显示在控制台的独立一行中。

迭代项

通常你会希望在循环中同时获取字典的键和值。items()方法返回字典的所有键值对。在编写任何循环代码之前,可以使用items()方法打印courses字典中的项:

print(courses.items())

这是控制台输出(由于输出过长,使用省略号表示不能放在一行上):

[('web design', 'Prof. Ncube'), ('game development', ...

你应该能够识别出每一对键值,它们被括在括号内。每对键值之间的括号表示一个元组。本书不会详细讨论元组,所以暂时可以将它们视为与列表等效。

添加这个循环,将每个键值元组打印在控制台的单独一行:

. . .for kv in courses.items(): print(kv)

当你在for循环中使用items()方法时,Python 会将一个元组分配给你的循环变量。我将其命名为kv,是键值的缩写,但你可以根据自己的喜好命名。例如,在代码艺术迭代中,kv等于('code art', 'Prof. Sato')。运行示例以确认控制台显示每个键值对,并且括号和逗号都显示出来。

为了方便遍历字典项,Python 允许你在 forin 之间包括两个变量,一个用于键,另一个用于相应的值。你可以根据自己的喜好命名这些变量,但赋值的顺序始终是 key 在前,value 在后;这与元组中的顺序一致。添加此示例以将课程名称和教授姓名分配给不同的变量。此外,此代码包括一个 sorted() 函数:

. . .for course1, prof2 in sorted(courses.items()): print('{} coordinates the {} course.'.format(prof, course))

每次迭代时,Python 将键(课程名称)赋值给第一个变量 1,将值(教授姓名)赋值给第二个变量 2。控制台应显示以下内容:

Prof. Sato coordinates the code art course.
Prof. Smith coordinates the game development course.
Prof. Ncube coordinates the web design course.

请注意,sorted() 函数始终对键进行操作,因此句子是按照课程名称的字母顺序排序的,而不是按照教授的名字。要反转顺序,可以添加 reversed() 函数:

. . .**#**for course, prof in sorted(courses.items()):for course, prof in reversed(sorted(courses.items())): print('{} coordinates the {} course.'.format(prof, course))

现在,code art 课程将作为最后一行显示在你的控制台中。

使用 JSON

JavaScript 对象表示法(JSON)来源于 JavaScript,但它是一种独立于语言的数据格式。许多编程语言支持 JSON,包括 Python,它在 Web 开发中非常流行。你可以使用 JSON 将类似字典的数据存储在纯文本文件中,以键值对的形式,构建嵌套的字典和列表风格的结构。

对于这次练习,你将使用 JSON 来格式化纯文本文件中的数据,就像在第七章中使用 CSV 一样,只是语法不同,并且文件扩展名是 .json。Python 的内置 json 模块将处理你读取的数据。如前所述,JSON 语法并不像 CSV 那样简单,但它更具描述性和多功能性。

理解 JSON 语法

为了理解 JSON 语法的工作原理,我们可以将其与 CSV 进行对比。在第七章中,你将专辑曲目列表(The Dark Side of the Moon)以 CSV 格式进行了存储。以下是该文件的简化版本:

location,title,creator,album,trackNum
file:///music/SpeakToMe.mp3,Speak to Me,Pink Floyd,The Dark Side of the Moon,1
file:///music/Breathe.mp3,Breathe,Pink Floyd,The Dark Side of the Moon,2. . .

第一行包含字段标题。第二行及以后的行提供每个曲目的详细信息。

以下是相同(简化版)曲目列表,格式化为 JSON:

[  { "location": "file:///music/SpeakToMe.mp3", "title": "Speak to Me", "creator": "Pink Floyd", "album": "The Dark Side of the Moon", "trackNum": 1 }, { "location": "file:///music/Breathe.mp3", "title": "Breathe", "creator": "Pink Floyd", "album": "The Dark Side of the Moon", "trackNum": 2 }, . . .

每个值都有一个对应的键——就像 Python 字典一样!如果你研究代码,你会发现它看起来像是 Python 中的字典列表。然而,JSON 和 Python 数据结构语法之间存在一些微妙的差异。例如,在 JSON 中,字符串必须使用双引号;而在 Python 中,你可以选择使用单引号。术语上也略有不同。在 JSON 中,大括号表示对象(而不是字典),而方括号表示数组(而不是列表)。你如何命名键和如何嵌套元素是由你决定的。

由于这是一个单个专辑的曲目列表,每个曲目都具有相同的 creatoralbum 信息,这看起来有些冗余。为了避免重复,你可以按以下方式重新结构化你的 JSON:

{  "creator": "Pink Floyd",   "album": "The Dark Side of the Moon", "tracklist": [ { "location": "file:///music/SpeakToMe.mp3", "title": "Speak to Me", "trackNum": 1 }, { "location": "file:///music/Breathe.mp3", "title": "Breathe", "trackNum": 2 }, . . .

新结构将曲目嵌套在 tracklist 内。creatoralbum 信息被放置在结构的顶部层级,因为它适用于每个曲目。

你可以编写自己的 JSON 数据、动态生成它,或者从网上获取它。

使用 Web APIs

大量的 JSON 数据库,包括音乐元数据到猫咪事实,都可以通过 Web APIs 获得。Web 应用程序编程接口(API) 是一种基于 Web 的服务,您可以用它来请求或发布数据。例如,你可以从 Twitter API 请求数据,生成一个衡量你推文频率的图表,或者编程一个自动发布代码艺术推文的 Twitter 机器人。

本书不涵盖如何使用 Web APIs。不过,如果你想探索 Web APIs,应该了解一些基本内容。每个 API 的工作方式略有不同,这意味着你需要查阅特定服务的开发文档。许多 APIs 可以通过 URL 直接访问,这允许你使用浏览器与它们进行交互。例如,OpenAQ 提供来自世界各地的空气质量数据。如果你在浏览器中输入以下 URL,你将得到挪威每个城市的空气质量数据的 JSON 摘要:api.openaq.org/v1/cities?country=NO

api.openaq.org 部分是 API 的域名。/v1 表示你正在使用第 1 版,即该 API 的第一次发布。/cities 部分请求 OpenAQ 数据库中每个城市的数据,但 ?country=NO 限制了这些城市只包括挪威的城市。要获取数据的副本,请使用浏览器菜单中的 保存页面为 选项,或将内容复制并粘贴到任何文本编辑器中。

你还可能会遇到提供 CSV 和 XML 数据的 API。

CSV、JSON 和 XML 各有优缺点,因此在选择最适合你项目的格式时,要权衡每种格式的相对优点。CSV 的优点在于其简洁性,但它无法支持层次结构的数据。与 JSON 不同,JSON 允许你在对象内嵌套多个层级的对象,而 CSV 限制每个字段只有一个值。XML 是一种成熟、广泛支持且灵活的数据交换格式,但有时它会显得过于复杂和臃肿。JSON 提供了一种折衷方案,并且在 Web 上变得越来越流行,因为它的语法比 XML 更简洁。

读取 JSON 数据

当你的草图运行时,你可以读取 JSON 数据。在这个例子中,你将使用存储在 JSON 文件中的咖啡数据生成一个图表。创建一个名为 coffee_chart 的新草图,并创建一个 data 子文件夹,然后完成以下步骤:

  1. 打开浏览器并访问 github.com/tabreturn/processing.py-book/

  2. 转到 chapter-08-dictionaries_and_json

  3. 下载 data.zip 文件。

  4. 解压 ZIP 文件,并将 coffees.json 移动到草图的 data 子文件夹中。

下面是 coffees.json 文件内容的一个片段:

[ {  "name": "Espresso",  "ingredients": [ {"ingredient":"espresso"1, "quantity":30 2, "color":"#221100"3} ] }, . . . {  "name": "Irish Coffee", "ingredients": [ {"ingredient":"espresso", "quantity":60, "color":"#221100"},  {"ingredient":"whiskey", "quantity":40, "color":"#FFCC77"},  {"ingredient":"whippedcream", "quantity":20, "color":"#FFFFFF"} ] },
]

每个顶层对象包含不同类型咖啡的详细信息。这段代码展示了两种咖啡:EspressoIrish Coffee。这两种是第一种和最后一种配方;总共有九种咖啡。每个ingredient对象有三个键值对:配料名称 1、数量(以毫升为单位)2、填充颜色 3。请注意,这些数量不一定准确,因此最终的图表可能不会让咖啡师和咖啡爱好者印象深刻,但它会看起来相当酷。

下一步是从coffees.json文件加载数据。Python 的open()函数可以打开任何文件,无论是纯文本文件还是其他文件,并返回一个文件对象。对于 JSON 文件,可以使用内置的json模块将文件对象加载到 Python 数据结构中。将以下代码添加到你的草图中:

import json
jsondata = open('coffees.json')
coffees = json.load(jsondata)

import行导入了json模块。open()函数打开 JSON 文件并将文件对象赋值给变量jsondatajson.load()函数将 JSON 转换为 Python 数据。为了确认它是否正常工作,可以打印出爱尔兰咖啡中的威士忌数量:

print(coffees[8]['ingredients'][1]['quantity']) # 40

爱尔兰咖啡是九种咖啡列表中的最后一个元素,因此它的索引是8。威士忌内容是第二个配料(['ingredients'][1])。最终的['quantity']表示配料的数量值,40 毫升。

当你通过使用loadStrings()获取 CSV 数据时,所有的数据类型都是字符串,包括数字。你必须使用int()函数将数值转换为整数,然后才能进行任何算术运算;要创建列表,你必须使用split()函数。然而,json模块会为你处理所有这些转换。像40这样的 JSON 值,没有小数点或引号,会被解释为整数;方括号中的逗号分隔值会自动转换为 Python 列表;等等。现在你可以在 Python 中访问数据,并使用它来渲染图表。

挑战 #9:咖啡图表

你将通过使用九个杯子,按照 3 × 3 的网格方式来可视化所有九种咖啡的数据。图 8-3 显示了最终结果的截图。

f08003

图 8-3:完整的咖啡图表

添加以下代码来定义显示窗口的大小、背景颜色和一些变量,并布局九个空杯子:

. . .size(800, 800)background('#004477')
mug = 120
spacing = 230
col = 1translate(100, 100)for coffee in coffees: 1 # ingredients code goes here # mug strokeWeight(5) stroke('#FFFFFF') noFill() square(0, 0, mug) arc(mug, mug/2, 40, 40, -HALF_PI, HALF_PI) arc(mug, mug/2, 65, 65, -HALF_PI, HALF_PI) # label fill('#FFFFFF') textSize(16) label = coffee['name'] text(label, mug/2-textWidth(label)/2, mug+40) 2 if col = 3: translate(spacing*-2, spacing) col = 1 3 else: translate(spacing, 0) col += 1

for语句之前的代码定义了草图的主要参数,如显示窗口大小和背景颜色。mugspacing变量分别控制马克杯的大小和间距;col变量作为列计数器。在for循环中的注释指出了绘制马克杯和标签代码的开始位置。绘制每个马克杯 3 之后,Processing 会将绘图空间向右移动 230 像素,并将col加 1。当col达到 3 2(每第三个马克杯)时,绘图空间会被移回到显示窗口的左边缘,并下移一行,col重置为 1。配料的代码已经为你留好,注释指出了你应该编写代码的位置 1。

运行草图。你应该能看到九个空的带标签的马克杯。现在,完成图表,使其看起来像图 8-3。如果你需要帮助,可以通过github.com/tabreturn/processing.py-book/tree/master/chapter-08-dictionaries_and_json/coffee_chart/查看解决方案。

摘要

在本章中,你学会了如何将项目集合组织成字典,这样可以将值与有意义的键关联起来。此外,你还将字典和列表结合起来,创建了更直观的数据结构。你还学会了如何定义、访问、修改和嵌套字典,以及如何通过键和值来遍历字典。

本章还介绍了 JSON。你了解了它与 Python 字典和列表的相似之处,以及如何读取 JSON 数据。你可以将字典和列表数据存储在 JSON 文件中,从而将 Python 代码与数据分离。如果你正在寻找有趣的数据来进行处理,许多在线资源都提供了免费的 JSON 数据集供你访问。

在第九章,你将学习如何定义和使用函数,它们是命名的代码块。你决定为函数命名,每当你想运行一个函数时,通过其名称来调用它。这有助于减少代码中的重复,因为你可以多次调用一个单行函数,而无需重复编写多行代码。可以将函数看作是可重用的代码块,它们将使你的草图更加高效,易于维护。你将编写函数,包括一些生成椭圆和波浪运动的函数,然后使用这些函数来编程动画效果,运用三角函数进行计算。

第九章:函数和周期性运动

随着你的程序变得越来越复杂,代码行数会增加,你也会开始重复相同或相似的代码。通过使用函数,你可以将程序分成具有名称的可重用代码块。这使得代码更加模块化,允许你在不需要重写代码的情况下重用某些代码行。

你已经使用过许多 Processing 函数,例如size()print()rect(),在本章中,你将学习如何定义自己的函数。作为一个例子,Processing 没有绘制菱形的函数,但你可以创建一个。你可以决定这个函数的名称以及它接受的参数。也许你的diamond()函数接受 x、y、宽度、高度和可选的旋转角度参数。

你还将创建生成椭圆形和波动类型运动的函数,这将涉及一些三角学内容。你将使用 Processing 的内置函数来执行这些数学计算,从而结合正弦和余弦函数。如果三角学的提法让你想起了数学课上那些让人不快的回忆,不妨深呼吸放松一下。这将是一个实用且可视化的重新介绍,Processing 将为你计算所有的数字。

定义函数

合理命名的函数使得代码更容易理解和使用。一个 1000 行的程序可能会让人难以理解,尤其是对于那些没有编写它的人来说。

想象一下编写一个音乐播放器。你可能会创建一个名为play()的函数,执行加载和播放 MP3 文件所需的 20 行左右的代码。当你需要播放一首歌曲时,只需使用文件参数调用play()函数,例如play('track_1.mp3')。在定义了play()函数之后,你无需关心它是如何操作的,其他与代码一起工作的人也不需要。除此之外,你还可以为stop()pause()skipBack()skipForward()定义函数。

在本节中,你将学习如何使用def关键字定义函数,以及如何处理函数参数。你可以将这些称为用户定义的函数,以便将它们与 Python 和 Processing 中内置的函数区分开来。

创建一个简单的对话框函数

让我们从一个简单的函数开始,这个函数不接受任何参数,并在控制台中绘制像漫画条形图那样的对话框。你已经使用过不需要参数的函数,比如 Processing 中的noFill(),它仅依赖函数名和括号。相反,一个像fill()的函数则需要至少一个参数,比如十六进制的颜色值。

你的对话框函数将形成一个轮廓,使用纯文本字符,围绕一个标题。一旦这个功能实现,你就可以定义一个更动态的函数,接受一系列参数,在显示窗口中绘制对话框。

创建一个新的草图,并将其保存为speech_bubbles。添加以下代码,首先在控制台打印一个问题,然后在五秒钟后通过对话框显示答案:

wait = 50001 print('1\. What do you get if you multiply six by seven?')2 delay(wait)print(' ------------------- ')print('| The answer is 42! |')print('| ------------------ ')print('|/')

当你运行草图时,你应该立即看到问题出现在控制台中。Processing 的delay()函数会暂停程序 5,000 毫秒(五秒),然后使用接下来的四行打印语句在对话框中揭示答案。运行草图以确认这一点:

1\. What do you get if you multiply six by seven? ------------------- 
| The answer is 42! |
| ------------------ 
|/

这可能看起来不是最令人信服的对话框,但目前勉强可以使用。

对你的代码做以下更改,以定义一个打印答案的函数:

wait = 5000def printAnswer(): print(' ------------------- ') print('| The answer is 42! |') print('| ------------------ ') print('|/')print('What do you get if you multiply six by seven?')delay(wait)
printAnswer()

def关键字定义了一个新函数。你可以给这个函数起任何名字,但要确保名字具有描述性。像变量名一样,函数名应只包含字母数字和下划线字符,并且必须以字母或下划线开头;在这种情况下,我选择了printAnswer。在def行末尾始终包括括号和冒号。四行print()语句位于函数定义的主体部分,这是def行下面的缩进代码。函数不会执行打印语句,直到你调用它。在最后一行,程序必须揭示答案时,调用了printAnswer()函数。

当你运行草图时,程序应该和以前一样工作,首先打印问题,然后在五秒钟后通过对话框显示答案。

PEP 8 风格指南建议“函数名应为小写,必要时使用下划线分隔单词以提高可读性。”换句话说,printAnswer()函数应该命名为print_answer()。然而,当已有的命名风格已经建立时,通常更倾向于保持内部一致性。

我选择了驼峰命名法的函数名,以符合 Processing 内置函数的命名惯例,如noFill()pushMatrix()。正如第一章所提到的,驼峰命名法将多个单词合并成一个单词,并使用大写字母开始第二个及之后的单词。这种风格也称为混合大小写,有时也叫小驼峰命名法(与大驼峰命名法相对)。

在草图的末尾添加第二个问题:

. . .delay(wait/2)print('2\. How many US gallons are there in a barrel of oil?')delay(wait)
printAnswer()

在显示问题 1 的答案之后,程序等待两秒半,然后打印问题 2。问题 2 的答案将在五秒后揭示。答案再次是 42,但不需要重新输入显示对话框的四行代码。相反,你可以第二次调用printAnswer()函数。

你可以添加任意数量的问题。如果每个问题的答案都是 42,你可以调用printAnswer()函数来显示答案。如果你想更改所有对话框的样式——例如,使用不同的字符来表示边框——编辑printAnswer()定义中的代码。你只需要在一个地方修改代码,就能影响每个对话框。

对于每个答案,您有一个整洁的单行函数调用,函数名称表明它的作用。其他程序员无需了解printAnswer()函数的内部工作原理即可使用它,但如果需要,他们可以通过查看定义代码了解它是如何工作的。

在继续进行下一部分之前,将代码顶部的wait值设置为0

wait = **0**. . .

这个更改取消了delay()函数的效果,因为零延迟时间意味着没有延迟。因此,草图不会暂停,您添加的下一部分代码可以立即运行。

printAnswer()函数仅限于在控制台中绘制气泡,并且它总是打印相同的答案 42,因此接下来,您将定义一个可以接受参数的函数。

使用函数绘制复合形状

要定义一个在显示窗口中绘制带有形状和文本的气泡函数,请继续在您的speech_bubbles草图中工作。首先,您需要一张图片来放置气泡。

我选择了扬·范·艾克的阿尔诺芬尼肖像作为本示例,因为这幅画有三个气泡候选人:一名男子、一名女子和一只狗。而且它是公有领域的。图 9-1 展示了左侧的原始画作,以及右侧您将要完成的效果。

f09001

图 9-1:原始阿尔诺芬尼肖像,1434 年(左);带有气泡的版本(右)

您可以从维基百科下载阿尔诺芬尼肖像图片(en.wikipedia.org/wiki/File:Van_Eyck_-_Arnolfini_Portrait.jpg);561 × 768 像素的分辨率就足够了。如果您想使用不同的图片也可以;只需选择至少有三个人物的图片。

创建一个新的data子文件夹,并将您的图片添加到其中;然后添加以下代码以加载并显示它:

. . .size(561, 768)
art = loadImage('561px-Van_Eyck_-_Arnolfini_Portrait.jpg')image(art, 0, 0, width, height)

如果您没有使用阿尔诺芬尼肖像,请相应调整size()loadImage()的参数。

运行草图以确认图片跨越了您的显示窗口。

定义并调用一个新的气泡函数,将以下代码添加到草图的末尾:

. . .def speechBubble(): x = 190 y = 150 txt = 'Check out my hat!' noStroke() pushMatrix() translate(x, y) # tail fill('#FFFFFF') beginShape() vertex(0, 0) # tip vertex(15, -40) vertex(35, -40) endShape(CLOSE) # bubble textSize(15) by = -85 bw = textWidth(txt) pad = 20 rect(0, by, bw+pad*2, 45, 10) fill('#000000') textAlign(LEFT, CENTER) text(txt, pad, by+pad) popMatrix() 
speechBubble()

如果您使用的是不同的图片,请调整xytxt变量。xy变量控制气泡的位置——具体来说,是指附着在气泡上的“尾巴”尖端的 x-y 坐标。在绘制任何内容之前,translate()函数会重新定位绘图空间,使得这个尖端的顶点坐标为(0,0);其他尾巴顶点和气泡则相对于这个点进行定位。

txt变量定义了气泡内显示的文本。您可以使用任何喜欢的txt字符串,但请保持简短。气泡不支持多行标题。

bubble 注释下方的代码绘制了一个位于尾部上方的圆角矩形气泡。rect() 函数包含第五个参数(10),该参数控制角的半径。这个值越大,角就会变得越圆。结果是一个带有左下角尾巴的圆角矩形语音气泡(图 9-2)。

f09002

图 9-2:语音气泡尾部的尖端坐标为 (190, 150)。

你可以调用 speechBubble() 函数 100 次,但视觉效果始终相同,因为每个语音气泡都覆盖在前一个上,大小相同,文本相同,位置也相同。但是,如果你每次调用 speechBubble() 函数时都修改 xytxt 变量,你就可以自定义 x 坐标、y 坐标和标题。你可以通过向函数定义中添加参数来实现这一点,这样你就可以在函数调用中通过不同的参数传递值给函数。

添加参数和占位符

现在你将编辑 speechBubble() 的定义,使得函数可以接受三个参数,这样你就可以将坐标和标题值传递给函数,进而操作每个语音气泡的外观。参数被赋值给相应的占位符,但稍后会详细讲解这些内容。

当前,有三个变量控制语音气泡的外观:xytxt。要通过参数控制这些变量值,可以按如下方式调整你的函数定义:

. . .1 def speechBubble(**x, y, txt**): **#**x = 190 **#**y = 150 **#**txt = 'Check out my hat!' . . .2 speechBubble(**190, 150, 'Check out my hat!'**)

定义括号现在包含三个参数:xytxt 1。参数是一个占位符,代表通过参数传入的值。这些参数在函数的局部作用域内可用;换句话说,Python 只能在 speechBubble() 函数块内访问 xytxt。你需要注释掉(或删除)旧的 xytxt 行,以避免覆盖你通过函数调用传递的值 2。

因为你有三个参数,所以在调用 speechBubble() 函数时必须提供三个参数。第一个参数 190 被赋值给参数 x,第二个参数 150 被赋值给参数 y,依此类推,按照参数在 def 行中出现的顺序。这些被称为位置参数,因为参数的顺序决定了每个参数所分配的值(图 9-3)。

f09003

图 9-3:位置参数

运行草图以确认视觉效果没有变化。尝试测试不同的参数来改变语音气泡的外观。

调用第二个 speechBubble() 函数:

. . .
speechBubble(315, 650, 'Woof')

第一个和第二个(x 和 y)参数将语音气泡定位在狗的上方。第三个参数指定标题必须为“汪汪”(图 9-4)。

f09004

图 9-4:第二个语音气泡

现在你已经有了一个可以接受位置参数的 speechBubble() 函数。然而,如果你使用关键字参数,你也可以按任意顺序调用此函数。

使用关键字参数

当你调用一个函数时,可以通过使用关键字参数明确指定每个值对应的参数。这些参数包括一个关键字和一个值。每个关键字的名称来自函数定义中的参数。考虑以下示例,其中两行代码产生相同的结果:

speechBubble(315, 650, 'Woof') # positional arguments
speechBubble(txt='Woof', x=315, y=650) # keyword arguments

第一个 speechBubble() 调用使用了位置参数方法。第二个调用使用了关键字参数;注意每个值前面都有一个关键字。Python 使用你在函数调用中的关键字来匹配值和参数(图 9-5)。

f09005

图 9-5:关键字参数

这意味着你可以随意排列函数调用中的参数顺序。只要确保你的关键字与函数定义中的参数名称完全一致。

设置默认值

当你定义一个函数时,可以为每个参数指定一个默认值,如果你在函数调用时省略了某个参数,Python 就会使用这个备份值。这种行为对于定义可选参数很有用。例如,rect() 函数可以接受一个可选的第五个参数,用于设置圆角半径。如果你调用 rect() 函数时只传入四个参数,默认会得到一个具有 90 度角的矩形,这是用户通常更常见的需求。但是,如果你提供第五个参数(值为非零的其他数值),你将得到一个带圆角的矩形。

使用等号为参数指定默认值。例如,下面的代码为 txt 参数添加了默认值 'Hello'

. . .def speechBubble(x, y, txt**='Hello'**): . . .

默认的 txt 参数是一个字符串,但你可以使用任何你喜欢的数据类型,包括数字和列表。

你现在可以使用两个位置参数来调用 speechBubble() 函数,让 txt(第三个参数)使用默认值:

. . .
speechBubble(445, 125)

445125xy 的位置参数。由于没有第三个参数,txt 默认为 'Hello',如函数定义所示。结果(图 9-6)是一个位于女性头顶上的对话框,内容为'Hello'

f09006

图 9-6:使用默认的 txt 参数(即 Hello)绘制一个对话框

要将 Hello 替换为 Meh,请使用三个参数调用 speechBubble() 函数:

. . .speechBubble(445, 125, **'Meh'**)

因为你为 txt 参数提供了位置参数,所以该女性的对话框现在会显示“Meh”。

那位女士显然对她伴侣的帽子并不十分印象深刻,所以她可能不想冒犯他。此时使用思考气泡可能更为合适(图 9-7)。

f09007

图 9-7:一个对话框(左)和一个思考气泡(右)

要绘制思维气泡,请修改 speechBubble() 函数,以绘制一串小圆圈,而不是三角形尾巴。然而,你希望 speechBubble() 函数默认绘制语音气泡,因为语音气泡比思维气泡更常见。

向函数定义中添加一个额外的 type 参数:

. . .def speechBubble(x, y, txt='Hello'**, type='speech'**): . . .

现在你有两个带默认值的参数。注意,这些参数位于没有默认值的参数之后。如果你定义任何带默认值的函数,应该将这些参数放在列表的末尾。

下一步是修改函数体,特别是 tail 注释下方的部分。type 参数必须决定 Processing 是否绘制三角形尾巴或一串圆圈。修改代码如下:

 . . . # tail if type == 'speech': fill('#FFFFFF') beginShape() vertex(0, 0)  # tip vertex(15, -40) vertex(35, -40) endShape(CLOSE) elif type == 'thought': fill('#FFFFFF') circle(0, 0, 8) circle(10, -20, 20) . . .

如果 type 参数等于 'speech'(函数定义中分配的默认值),if 语句代码将绘制一个三角形尾巴。每当函数调用中包含 type 参数为 'thought' 时,elif 语句将绘制一串两个圆圈。编辑你的函数调用,看看效果:

. . .speechBubble(445, 125, 'Meh'**, 'thought'**)

thought 参数将 speechBubble() 函数切换到“思维气泡模式”。如果省略此参数,函数将默认绘制带尾巴的语音气泡。运行草图以确认结果与图 9-7 一致。

混合位置参数和关键字参数

你可以使用位置参数来传递 xy 坐标,省略 txt 参数,并为 type 添加一个关键字参数。这样,Python 可以使用 txt 的默认值('Hello'),但将其渲染为思维气泡。例如,你可能想将狗的语音气泡替换为一个写着'Hello'的思维气泡。一个选项是明确地在函数调用中包含 'Hello' 作为第三个参数——完全的定位方式。例如:

speechBubble(315, 650, 'Hello', 'thought')

这里的每个参数都对应一个参数。尽管如此,这看起来有些多余,因为 'Hello' 已经是第三个参数的默认值。如果你在函数调用中省略 'Hello' 参数,Processing 将绘制一个带有 thought 字样的 speech 气泡:

# a speech bubble that says, thought
speechBubble(315, 640, 'thought')

回想一下,第三个位置参数是 txt 参数,省略第四个参数意味着 Python 必须采用第四个 type 参数的默认值(语音气泡模式)。不过,这个问题有一个简单的解决方案;使用关键字参数,而不是依赖位置参数:

speechBubble(315, 650, **type=**'thought')

在这种情况下,你明确地声明了值 'thought' 属于 type 参数。你可能会注意到,如果你对每个值都使用关键字参数,那么可以以任何顺序排列参数。这是正确的,所以根据具体情况决定最适合的混合位置参数和关键字参数组合。

如果你在函数调用中缺少一个或多个必需的参数,Processing 会显示错误信息(图 9-8)。例如,如果你调用speechBubble()函数时没有任何参数,错误信息会显示你至少需要两个参数。

f09008

图 9-8:缺少参数的错误信息

如果你提供了太多参数,错误信息会显示speechBubble()函数最多只接受四个参数。

返回值

你可以使用一个函数来处理数据,然后让它返回结果给主程序。这与到目前为止你创建的函数不同,后者执行预定义的代码块后,才会恢复主程序的正常流程。

为了帮助解释这个区别,这里有一些代码,用来对比一个返回值的函数和一个不返回值的函数:

x = random(100)square(x, 40, 20)

这里使用了两个 Processing 函数:random()square()。第一个函数返回一个值;第二个函数则不返回值。random()函数生成一个从 0 到但不包括 100 的浮动值。random()函数返回该值,并将其分配给一个名为x的变量。square()函数在显示窗口中绘制一个正方形;它不返回值。

要定义一个返回值的函数,使用return关键字。举个例子,创建一个新的名为shout()的函数。这个函数接受一个字符串参数,然后将该字符串转换为大写并在结尾加上三个感叹号。将以下代码输入到speechBubble()调用之前,以确保shout()定义位于任何shout()函数调用之前:

. . .def shout(txt): return txt.upper() + '!!!'. . .

return语句中,upper()方法将分配给txt的字符串转换为大写;最终结果是该字符串与三个感叹号的拼接。一旦 Python 处理了return语句,它会立即退出函数。换句话说,如果你在return语句下面的shout()定义中添加任何其他代码,Python 会忽略它。

你可以使用这个函数在任意对话框中的文本上添加强调。以下是一个示例:

speechBubble(190, 150, shout('Check out my hat'))

shout()函数将字符串转换为“CHECK OUT MY HAT!!!”之后,才会传递给speechBubble()函数。这将shout()函数包装在参数中,避免了创建中间变量的需要,之后你可以将其传递给speechBubble()函数。

这是一个简单的示例,用来介绍return关键字的作用。许多返回值的函数执行更复杂的任务,例如 Processing 的sqrt()函数,它计算任意数字的平方根。

定义周期运动的函数

在本节中,你将学习如何通过定义使用三角函数的函数来模拟 Processing 中的周期性运动,以绘制圆形图案和波动。在物理学中,周期性运动是指以规律间隔重复的运动,例如摆动的钟摆、水中传播的波浪,或者围绕地球运动的月亮。周期是完成一个运动循环所需的时间。月球绕地球的轨道周期大约为 27.3 天;时钟的秒针周期为 60 秒。

三角学,或称三角函数,是数学的一个分支,研究三角形,并使用各种数学函数,如正弦和余弦,来计算角度和距离。它在许多编程领域也有应用。例如,包含物理的游戏必须不断计算运动中物体的位置和速度,这些计算涉及三角形。

三角函数在控制方向和瞄准行为方面也非常有用。例如,如果你知道图 9-9 中玩家和敌人炮塔的 x-y 坐标,你可以计算出如何旋转敌人炮塔以瞄准玩家。

你将使用直角三角形,通过正弦和余弦函数来计算圆周上的点。这些点的坐标就是你用来模拟平滑周期性运动的基础。

f09009

图 9-9:如果敌人炮塔在数学课上听讲就好了。

创建一个新的草图并将其保存为periodic_motion。添加以下代码来设置绘图空间:

def setup(): size(800, 600)def draw(): background('#004477') noFill() strokeWeight(3) stroke('#0099FF') line(width/2, height, width/2, 0) line(0, height/2, width, height/2) # flip the y-axis scale(1, -1) translate(0, -height) # reposition the origin translate(width/2, height/2)

上述代码通过使用setup()draw()函数,结构化了一个动画草图,其中有两条(淡蓝色的)线条在显示窗口的中心交汇。y 轴被翻转,所以 y 坐标随着向下移动而减少;我稍后会详细解释为什么这样做。最后的translate()函数将坐标系统移位,使原点(0, 0)位于显示窗口的中心。这意味着显示窗口左边缘的 x 坐标是–400,右边缘的 x 坐标是 400,顶部边缘的 y 坐标是 300,底部边缘的 y 坐标是–300(见图 9-10)。修改后的坐标空间,带有翻转的 y 轴,现在像一个常规的笛卡尔平面,拥有四个象限,允许你绘制任何范围在(–400, –300)和(400, 300)之间的 x-y 坐标。

你可能在数学课上遇到过这种系统,这也是我以这种方式设置坐标空间的原因。你将把它作为一个平台,来实验椭圆运动和波动运动,但首先,你可能需要简要复习一下三角函数。

f09010

图 9-10:带有四个象限的笛卡尔平面

三角函数介绍

正弦余弦正切是三种常见的三角函数。这些是数学(而非编程)函数,但你可以通过 Processing 的内置三角函数在 Python 中使用它们。sin、cos 和 tan—它们通常的缩写—是基于从直角三角形中得到的比值(见图 9-11)。直角三角形(或直角三角形)有一个角度恰好为 90 度,通常用一个小方块表示。θ符号,theta,通常用来表示未知角度。

f09011

图 9-11:一个直角三角形

如果你知道这个三角形的任意两条边的长度,你可以计算出θ的大小。根据你掌握的边长,你将使用 sin、cos 或 tan 来进行计算。SOHCAHTOA,按发音规则为so-ka-toe-uh,是一个方便的助记法,帮助你记住以下三角比率:

  1. SOH sin(θ) = 对边 / 斜边

  2. CAH cos(θ) = 邻边 / 斜边

  3. TOA tan(θ) = 对边 / 邻边

作为一个例子,如果你知道图 9-11 中对边和斜边的长度,你可以通过使用 sin(θ)来求得角度θ。如果你知道邻边和斜边的长度,使用 cos(θ)。你也可以重新排列这些方程式,在已知θ和某个边长的情况下,求出未知边的长度。我稍后会再提到这一点。

你将应用 sin 和 cos 来解决一个简单的例子,计算沿着圆周的 x-y 坐标。首先,画一个圆,圆心位于(0, 0),半径为 200。添加一条从(0, 0)开始的线段,长度与圆的半径相同,并旋转 1 弧度:

. . .
radius = 200
theta = 1def draw(): . . . circle(0, 0, radius*2) stroke('#FFFFFF') pushMatrix() rotate(theta) # approximately 57.3 degrees line(0, 0, radius, 0) popMatrix()

代码将圆形呈现为淡蓝色的轮廓。一个白色的线段从圆心延伸到圆周,长度为半径;这形成了一个 1 弧度(大约等于 57.3 度)的角度,如图 9-12 所示。注意,rotate()函数应用于线条时是逆时针方向,因为 y 轴是反向的。任务是计算白线与圆周相接的点 A 的 x-y 坐标。其他黄色标记揭示了你将基于其进行计算的直角三角形。

f09012

图 9-12:你将找到标记为 A 的点的 x-y 坐标。

观察到 A 点的 y 坐标等于对边的长度(或高度)。你知道角度(theta变量)和斜边的长度(radius),你可以用它们来计算对边的长度。记住,SOHSOHCAHTOA中表示sin(θ) = 对边 / 斜边

你已经知道θ和斜边的值,所以重新排列方程以求出对边:对边 = sin(θ) × 斜边

如果你用程序中的变量名替换占位符,这是y = sin(theta) * radius

为了计算 A 点的 x 坐标,你需要找出相邻边的长度(或宽度)。回想一下,SOHCAHTOA 中的 CAH 表示 cos(θ) = adjacent / hypotenuse,你可以将其重新排列为 x = cos(theta) * radius

将以下代码添加到 draw() 函数的末尾:

 . . . # white dot noStroke() fill('#FFFFFF') x = cos(theta) * radius y = sin(theta) * radius circle(x, y, 15)

cos()sin() 函数返回浮动值,范围从 –1 到 1,适用于不同的 theta 值。Processing 的三角函数使用弧度制,因此不需要将 theta 参数转换为度数。在这个例子中,theta 等于 1 弧度,cos()sin() 函数分别返回 0.54 和 0.84 的值(四舍五入到小数点后两位)。当你将 0.54 和 0.84 乘以半径值 200 时,你会得到一个 x-y 坐标(108, 168)。circle(x, y, 15) 函数通过这个 x-y 坐标对渲染一个白色点。运行草图,确认白色点位于 A 点位置,即白色线条连接到圆周边界的地方。

你可以调整 theta 值,将白色点移动到淡蓝色圆周上的不同点。要将点定位在原点正上方的 90 度位置,使用 theta = HALF_PI;要定位在 180 度位置,使用 theta = PI;以此类推。theta 值为 TAU 会让你回到起始点,在视觉上与 theta = 0 的点无区别。如果 theta 大于 TAU,则会发生环绕效果。换句话说,cos(TAU+1) 等同于 cos(1)

接下来的任务是让点开始移动。你不再需要白色线条;通过删除从 pushMatrix() 开始直到包括 popMatrix() 的代码来去除它。

圆形与椭圆形运动

你将从沿着圆周移动点开始(即圆周运动),并创建一个用户定义的函数来处理必要的数学运算。然后,你将使用这个相同的函数来创建圆形运动的螺旋变体。完成圆形和螺旋运动后,你将定义一个新的函数来实现椭圆运动。图 9-13 展示了每种运动的示例。

f09013

图 9-13:圆形(左)、螺旋形(中)和椭圆形(右)运动

圆形

回想一下,存储在名为 theta 的变量中的角度大小决定了白色点的位置。为了让点沿圆周逆时针方向移动,添加代码每次执行 draw 函数时递增 theta。包括一个 period 变量来控制增量大小:

. . .
period = 2.1def draw(): global theta 1 theta += TAU / (frameRate * period) . . .

在默认的 frameRate 为 60 fps,period 为 2.1 秒的情况下,theta 的增量大约是 0.05 1。这意味着每一帧,角度会延伸 0.05 弧度。运行草图来测试这一点。白色点应该在大约 2.1 秒内完成一圈圆周。

你增加 theta 的值时,点移动得越快。减小 theta 值会使点向相反方向(顺时针)移动。

定义一个名为circlePoint()的新函数,用于计算圆周上的点。在你的draw()函数中,将xy的代码行替换为circlePoint()函数调用:

def circlePoint(t, r): x = cos(t) * r y = sin(t) * r return [x, y]1. . .def draw(): . . . **#**y = sin(theta) * radius **#**x = cos(theta) * radius x, y2 = circlePoint(theta, radius) circle(x, y, 15)

circlePoint()的定义包括两个参数:t表示 theta(角度),r表示半径。由于该函数需要计算圆周上一些点的 x 和 y 坐标,因此它需要返回两个值。使用列表来返回多个值;你也可以使用字典(或元组)。

当你调用该函数时,Python 可以解包列表中的值并将其赋值给多个变量。为了触发这种解包行为,需要为每个列表项提供相应的变量,并用逗号分隔每个变量。在这种情况下,函数返回一个包含两个值的列表,这些值分别赋给变量xy。另外,你也可以将列表赋给一个变量,如a = circlePoint(theta, radius),但这时你必须通过a[0]a[1]来引用xy,这种方式不如前者简洁或直观。

螺旋

对于向外螺旋的运动(见图 9-13 中的中心图像),你可以使用随时间增加的半径值。以下是一个例子:

 . . . x, y = circlePoint(theta, frameCount) circle(x, y, 15)

请记住,frameCount是一个系统变量,包含自开始绘制以来显示的帧数。半径参数(frameCount)从 0 开始,随着动画的进展逐渐增大,导致点在螺旋轨迹上向外移动。由于每一次完整的旋转保持相同的周期,无论circlePoint()的半径如何,点在远离显示窗口中心时会加速。换句话说,点必须在相同的时间内覆盖更大的距离,因此它移动得更快。

椭圆

对于椭圆轨迹,你需要两个半径:一个用于水平轴,另一个用于垂直轴。这些半径控制椭圆形状的宽度和高度,从而指导白点的运动轨迹(见图 9-13 右图)。定义一个新的ellipsePoint()函数,参数包括角度、水平半径和垂直半径:

def ellipsePoint(t, hr, vr): x = cos(t) * hr y = sin(t) * vr return [x, y]. . .

函数体与circlePoint()函数类似。不同之处在于,你需要分别将xy值乘以hr(水平半径)和vr(垂直半径)参数。

以下ellipsePoint()函数调用使得点在椭圆轨迹上移动:

 . . . x, y = ellipsePoint(theta, radius*1.5, radius) circle(x, y, 15)

ellipsePoint()函数的第二个参数(水平半径)大于第三个参数(垂直半径),因此得到的椭圆在水平方向上比垂直方向更宽。

正弦波

正弦波 是一种几何波形,周期性地重复自身,就像一连串相连的 S 形曲线。该波形在许多数学和物理应用中都有出现。例如,你可以使用正弦波来模拟音乐音调、电波、潮汐和电流。

正弦波的形状是使用 sin() 函数形成的。图 9-14 描绘了一个黄色的正弦波。

f09014

图 9-14:正弦波

波长 是一个完整周期的长度,测量的是从波峰到波峰(或从波谷到波谷)的距离。波长与周期有关,但周期是指时间(完成一个周期所需的时间),而波长是指距离。

振幅 是从静止位置(y = 0)到波峰的距离。一个振幅为 0 的波将平铺在 x 轴上。你可以通过与淡蓝色圆形的半径进行比较,确定图 9-14 中的黄色波具有 200 的振幅。

为了模拟正弦波运动,请将以下代码添加到你的 periodic_motion 草图中。这与绘制圆形相同,但使用固定的 x 坐标:

. . .def draw(): . . . amplitude = radius y = sin(theta) * amplitude circle(0, y, 15)

波的 振幅 等于淡蓝色圆形的 半径,尽管你可以测试任何你喜欢的数值。白点的 y 坐标是通过 sin(theta) 乘以振幅计算得出的;x 坐标始终为 0。结果是一个从原点上下移动的白点。

运行草图并仔细观察点的加速和减速情况,就像图 9-14 中的波形穿过水面,点漂浮在其表面一样。当点接近波峰或波谷时,它开始减速,然后在转弯后加速;它在穿越 y 轴时移动得最快。

你可以使用此运动来绘制一整个移动的点波,或者模拟一个挂在弹簧上的重物(图 9-15)。

f09015

图 9-15:点阵波(左)和挂在弹簧上的重物(右)

每个示例的代码如下。你需要将其添加到 periodic_motion 草图的 draw() 块的末尾。如果你想在点阵波形上绘制弹簧和重物,可以添加两个代码列表,或者将其中一个列表替换为另一个。

绘制点状正弦波

使用循环绘制一整波点阵。总共有 51 个点,均匀分布在 x 轴上。每个点的 y 坐标基于一个比前一个点更大的 theta 值。

 amplitude = radius for i in range(51): 1 f = 0.125 * 2 t = theta + i * f 2 x = -400 + i * 16 3 y = sin(t) * amplitude circle(x, y, 15)

循环绘制 51 个点,从 x 坐标 -400 开始,x 坐标间隔为 16 像素 2。每个点的 y 值通过一个 theta 值计算,theta 值是比其左侧邻点大 0.125 * 2 弧度(即 0.25)。你可以将该乘数改为 1,表示一个波,占据显示窗口的宽度;将其保留为 2 表示两个波(如图 9-15 所示),将其改为 3 表示三个波,以此类推。我将变量命名为 f,表示频率,指的是事件在固定时间内重复的次数。

波长与频率成反比,因此随着频率的增加,波长会减小(波形开始变得更加尖锐)。波动从右向左传播,但点的水平位置不会改变。

模拟悬挂在弹簧上的重物

使用循环来绘制弹簧,弹簧是由顶点组成的形状。悬挂在弹簧末端的重物是一个矩形。调整填充和描边以绘制轮廓线,而不是填充形状:

 amplitude = radius y = sin(theta) * amplitude noFill() stroke('#FFFFFF') strokeJoin(ROUND) bends = 35 beginShape() for i in range(bends): vx = 30 + 60 * (i % 2 - 1) vy = 300 - (300 - y) / (bends - 1) * i vertex(vx, vy) endShape() rect(-100, y-80, 200, 80)

弹簧弯曲的紧角会产生锐利的接头,导致拉长的“肘部”。处理程序会在这些接头过长和过尖时将其裁剪,但在斜接(尖锐)和倒角(裁剪)接头之间跳跃会使动画看起来很差。为了防止这种情况,我将 strokeJoin 设置为 ROUND。在 beginShape()endShape() 函数内嵌套了一个循环,用于绘制锯齿形弹簧的顶点。

通常情况下,在这样的系统中,一些能量会被耗散或丧失,振幅应该随着时间的推移衰减。你可以通过每一帧减少(全局)radius 值来模拟这一点,直到它降到 0,这时重物将停下来。

现在你已经学会了如何从函数返回值,并将三角学应用于椭圆和波浪动画,接下来让我们看看通过组合波浪产生的特殊曲线。

利萨如曲线

在这一部分中,你将创建一个用于绘制由参数控制的利萨如曲线的函数。利萨如曲线——以法国物理学家朱尔·安托万·利萨如命名——是通过组合来自两个波的 x 和 y 坐标形成的。

你可以通过机械方法创建这些曲线,方法是设置一个 Y 形的摆钟,摆钟的末端挂着一个充满沙子的杯子。当杯子摆动时,沙子通过底部的孔流出,绘制出一条曲线。图 9-16 显示了这个装置的一个示例(左)和用沙子绘制的曲线(右)。标有 r 的点表示摆钟合并为一根弦的位置。摆钟上部和下部的比例、初始摆动的角度和力量决定了最终曲线的形状。

f09016

图 9-16:Blackburn 的 Y 形摆钟,出自 John Tyndall 的《声音》,1879 年(左),以及用沙子绘制的利萨如曲线(右)

首先,假设你有两个大小不同的圆(图 9-17)。圆形 A 的半径为 A,大小为 200 单位,圆形 B 的半径为 B,大小为 100 单位。

f09017

图 9-17:将来自不同圆的 x 和 y 值组合形成椭圆

结果椭圆(左下角)是通过使用圆形 A 的 x 坐标和圆形 B 的 y 坐标形成的。椭圆的宽度与圆形 A 一样,且高度与圆形 B 一样。这些数学计算相对简单,使用了你已经掌握的三角函数绘制椭圆的知识。

为了找到结果椭圆周长上任何点的 x-y 坐标,你可以使用以下公式:

x = cos(θ) × A

y = sin(θ) × B

创建一个新的草图,将其保存为 lissajous_curves,并添加以下代码以重现 图 9-17 中的椭圆:

def lissajousPoint(t, A, B): x = cos(t) * A y = sin(t) * B return [x, y]def setup(): size(800, 600) frameRate(30) background('#004477') fill('#FFFFFF') noStroke()
theta = 0
period = 10def draw(): global theta theta += TAU / (frameRate * period) # flip the y-axis and reposition the origin scale(1, -1) translate(width/2, height/2-height)  x, y = lissajousPoint(theta, 200, 100) circle(x, y, 15)

绘图空间的设置与之前的草图相同。你有一个倒置的 y 轴,原点已移至显示窗口的中心。theta 值每帧增量大约为 0.01,作为 lissajousPoint() 函数调用中的第一个参数。目前,这个函数执行的操作与你在 period_motion 草图中的 ellipsePoint() 函数完全相同——唯一的区别是函数名和变量名不同。

请注意,代码的 draw() 部分没有 background() 调用,因此 Processing 不会在每一帧清除画面。因为这个原因,移动的白点形成了一条连续的线条。运行草图,它应该以逆时针方向画出一个完整的椭圆(图 9-18)。

f09018

图 9-18:使用 ellipsePoint() 函数绘制椭圆

当 theta 达到 τ 弧度(大约为 6.28)时,椭圆已经完成,而 Processing 继续在现有线条上绘制。即使动画看起来已经完成,点仍在沿着周长移动。

下一步是修改 lissajousPoint() 函数,以便它能够绘制 Lissajous 曲线(而不是椭圆)。但首先,请考虑在波形方面发生了什么。研究 图 9-19,它将每个圆表示为一个波形,并注意每个波形上的点如何控制椭圆周长上的点位置。

f09019

图 9-19:以波形表示的圆形 A 和圆形 B

图 9-19 展示了圆形 A 的 x 坐标,作为一个余弦波,在 -1 和 1 之间振荡,且由 A 的圆半径(波幅)进行缩放。同样,圆形 B 的 y 坐标以正弦波的形式呈现,波幅为 B。

在 图 9-20 中,你可以看到点如何沿着波动形成椭圆的形状。

f09020

图 9-20:theta = 2(左),theta = 3(中),theta = 4(右)

目前,两个波的频率相匹配。换句话说,每个波完成一个周期的时间是相同的。结果是一个椭圆。

当波的频率不同时,就会出现 Lissajous 曲线。在图 9-21 中,Circle B 波的频率是 Circle A 波的两倍。跟随 Circle B 波的点必须在 Circle A 点完成一个周期的同样时间内完成两个周期。ab值(小写)分别表示频率 1 和 2。

f09021

图 9-21:Circle B 波的频率是 Circle A 波的两倍。

频率ab可以是 3 和 6,40 和 80,或者 620 和 1240。任何一对 1:2 的比例数字都会产生一个∞形状。这一点在你返回编写代码时将非常重要。你也可以用另一种方式理解这一点:在图 9-17 中,Circle B 点必须总是在 Circle A 点完成一个周长的同样时间内,完成两个周长的循环。

图 9-22 展示了点如何沿着波形移动,形成 Lissajous 曲线。

修改你的lissajousPoint()定义,添加一个参数表示频率a和频率b。将这两个参数作为乘数分别作用于xy线中的 theta(t):

def lissajousPoint(t, A, B**, a, b**): x = cos(t *** a**) * A y = sin(t *** b**) * B return [x, y] . . .

f09022

图 9-22:从左到右:theta = 2;theta = 3;theta = 4

现在,为你的函数调用添加参数ab

 x, y = lissajousPoint(theta, 200, 100**, 1, 2**)

运行草图,观察 Processing 绘制 Lissajous 曲线(图 9-23)。

f09023

图 9-23:通过使用lissajousPoint()函数绘制 Lissajous 曲线

ab参数决定了 Lissajous 曲线中的水平和垂直“叶片”的数量。记住,关键在于比例,因此12将产生与510相同的曲线。然而,后者将在更短的时间内完成曲线的绘制,甚至更大的数字会在点之间产生可识别的间距(否则它们会形成一条实线)。图 9-24 展示了几个ab参数的结果。尝试使用其他数字进行实验。

f09024

图 9-24:使用不同的ab参数绘制 Lissajous 曲线

你可以通过使用三角函数移动形状、点和线,创造出有趣的视觉图案。单纯地实验,没有预设的目标,可能会带来令人印象深刻的视觉效果。可以把这种编程方式看作是一场音乐即兴演奏会,演奏者即兴创作,直到他们偶然发现某种听起来不错的旋律。

下一步任务使用 Lissajous 曲线和line()函数来创建动画图案,这应该会给你提供一些有趣的创意。

使用 Lissajous 曲线创建类似屏幕保护程序的图案

在第六章中,你编写了一个简单的 DVD 屏幕保护程序;现在让我们使用 Lissajous 曲线创建一个更复杂的屏幕保护程序。屏幕保护程序的最初目的是“保护”屏幕。老式的阴极射线管(CRT)显示器容易发生烧屏现象:如果在同一个位置长时间显示相同的图形,它会留下永久的“鬼影”图像。现代显示器不容易发生烧屏现象,但许多人仍然使用屏幕保护程序,因为它们看起来很酷。

你将使用lissajousPoint()函数创建一个受流行屏幕保护程序设计启发的图案。图 9-25 展示了最终结果,随着图案在屏幕上扭动,线条和颜色平滑地变化。

f09025

图 9-25:基于 Lissajous 曲线的动画图案

该运动依赖于两条 Lissajous 曲线,使用line()函数在每条曲线的前端之间绘制一条直线。图 9-26 展示了这一过程是如何工作的。

f09026

图 9-26:在两条 Lissajous 曲线之间绘制直线

当然,你看不到曲线,只看到直线,但实际上是通过两次lissajousPoint()调用计算出你的line()函数的 x-y 坐标。当 theta 达到τ弧度时,Lissajous 曲线完成,运动开始重复。

将以下代码添加到draw()函数的末尾,在你的lissajous_curves草图中:

 . . . 1 for i in range(10): # curves t = theta + i / 15.0 x1, y1 = lissajousPoint(t, 300, 150, 3, 1) x2, y2 = lissajousPoint(t, 250, 220, 1, 3) # background color 2 fill(0x55000000) noStroke() rect(-width/2, -height/2, width, height) # line colorMode(HSB, 360, 100, 100) 3 h = (frameCount + i * 15) % 360 strokeWeight(7) stroke(h, 100, 100) line(x1, y1, x2, y2)

循环将绘制 10 条线——一条实线在前,后面跟着 9 条逐渐消失的线。你使用了两个lissajousPoint()函数,分别对应每条曲线(它们共同定义了每条线的两端的 x-y 坐标)。每次迭代时,Processing 都会绘制一个半透明的黑色方块,覆盖整个显示窗口,逐渐暗化之前迭代中的线条。

要定义半透明颜色,你使用 Processing 的0x表示法 2。十六进制值以0x开头,不带引号,后跟八个十六进制数字。前两位定义了alpha(透明度)成分;例如,11是高度透明的,而EE是高度不透明的。这个示例使用55,介于两者之间,但更接近透明端。其余六位是标准的 RGB 十六进制混合,在此为黑色(000000)。对于笔触颜色,设置colorMode()HSB(参见第 14 页的“颜色模式”)。在前 360 帧内,你可以使用frameCount每帧调整色相值 1 度。然而,frameCount很快会超过 360,所以你需要使用取模运算将其“环绕”回到 0 3。

运行草图以观察输出结果。

尝试不同的lissajousPoint()参数,或者添加新的曲线和线条;甚至可以尝试在三条曲线之间连接三条线,形成变形三角形。继续实验,看看你能创造出什么。

概述

在本章中,你学会了如何定义自己的函数,这样可以减少重复并帮助你构建更加模块化的程序。记住,命名清晰的函数会使你的代码更易于阅读和理解,不仅对你自己,也对其他任何处理代码的人。

你可以向任何函数添加参数,使其更具通用性,且函数调用会包含与这些参数对应的不同参数,以控制其工作方式。你可以使用位置参数和/或关键字参数来调用函数。对于可选参数,你可以定义包含默认值的参数,以便 Python 在必要时使用这些默认值。

你还可以定义返回值的函数,这意味着你可以使用一个函数来处理数据,并将结果返回给函数调用者。如果一个函数返回一个值,你可以将其赋值给一个变量。此外,你还可以将函数封装在一个参数中,以便处理并返回另一个函数所需的值。

本章还介绍了三角函数的概念以及如何使用它们来模拟周期性运动。你学习了内建的 Processing 三角函数,如 sin()cos(),并用它们绘制了圆形、螺旋、椭圆、正弦波和李萨如曲线。可以通过三角函数实验生成引人入胜的图案和运动,就像你在某些屏幕保护程序中看到的那样。

在下一章中,你将编写,通过它们来创建对象。这些技巧使你能够更高效地构建代码,尤其是在面对更大、更复杂的程序时,通过围绕现实世界中的对象来建模你的程序。你还将学习用于编程运动的向量

第十章:面向对象编程与 PVector

面向对象编程OOP)处理的数据结构被称为对象。你从类中创建新的对象,可以把类看作是一个对象模板,包含了一组相关的函数和变量。你为每一类想要处理的对象定义一个类,每个新对象都会自动采用你在类中定义的特性。OOP 结合了你迄今为止学到的所有内容,包括变量、条件语句、列表、字典和函数。OOP 提供了一种非常有效的方式,通过模拟现实世界的物体来组织你的程序。

你可以使用类来建模有形物体,如建筑物、人物、猫和汽车。或者,你可以用它们来建模更抽象的事物,如银行账户、个性和物理力量。虽然类定义了某类对象的通用特征,但你可以为每个创建的对象分配独特的属性以加以区分。在本章中,你将应用面向对象编程(OOP)技术来编写变形虫模拟程序。你将学习如何定义变形虫类,以及如何从中“生成”不同的变形虫。

你将通过模拟物理力量来编程变形虫的运动。为此,你将使用一个名为PVector的内置 Processing 类。PVector类是欧几里得向量的实现,包括一套用于执行数学运算的方法,你将使用这些方法来计算每个变形虫的位置和运动。

为了更好地管理代码,你将学习如何将程序拆分成多个文件。然后,你可以通过在 Processing 编辑器中使用标签在构成草图的文件之间切换。

使用类

就像是对象的蓝图。例如,考虑一个Car类,它可能默认指定所有汽车都有四个轮子、一个挡风玻璃,等等。某些特征,比如油漆颜色,在不同的汽车之间是可以变化的,所以当你使用Car类创建一个新的汽车对象时,你可以选择一个颜色。这些特征被称为属性。在 Python 中,属性是属于类的变量。你可以决定哪些属性有预定义的值(如四个轮子和挡风玻璃),哪些是在创建新车时分配的(如油漆颜色)。

通过这种方式,你可以使用单一类创建多辆不同颜色的汽车。图 10-1 说明了这一概念。Car类包括描述每辆车的油漆颜色、引擎类型和型号的属性。

f10001

图 10-1:Car类作为汽车对象的蓝图。

驾驶员通过转向、加速和刹车来控制车辆。因此,除了属性外,你的Car类还可以包含执行这些操作的定义,称为方法。在 Python 中,方法是属于某个类的函数,定义了该类可以执行的操作或活动。

现在,让我们定义一个包含一组属性和方法的Amoeba类,用于控制变形虫对象的外观和行为。你将使用该类创建许多变形虫。图 10-2 展示了你正在努力实现的变形虫模拟的最终结果。

f10002

图 10-2:完整变形虫模拟的截图

变形虫在显示窗口中移动时会摇摆和扭曲。这并不是科学上正确的变形虫表现形式,但它应该看起来非常酷。作为额外的挑战,你将添加碰撞检测代码,防止变形虫互相穿越或重叠。你将从一个基本的Amoeba类定义开始,然后随着任务的进展添加属性和方法。

定义一个新类

在 Python 中,你通过使用class关键字来定义一个类。你可以随意给类命名,但和变量和函数名一样,类名只能使用字母数字和下划线字符。由于不能使用空格字符,推荐的类命名约定是UpperCamelCase,即每个单词的首字母都大写,从第一个单词开始。

一开始,你的Amoeba类除了在控制台打印一行外,什么也不会做。开始一个新草图并将其保存为microscopic。定义一个新的Amoeba类:

class Amoeba(object): def __init__(self): print('amoeba initialized')

class关键字定义了一个新类。这里类名是Amoeba,后面跟着object,并用括号括起来,最后是冒号。

如果你运行这个草图,什么有趣的事情都不会发生,控制台将是空的。

你在类体内定义的函数称为方法Amoeba类包括一个特殊方法的定义,名为__init__(前后都有两个下划线)。这个方法是一些魔法方法中的一个,它们前后都有两个下划线,你不会直接调用它们。我稍后会详细讲解__init__()方法(以及self参数)。目前,你只需要知道的是,Python 会在每次创建新变形虫时自动运行__init__()方法。你使用这个方法在对象创建时设置属性并执行代码。

从类中创建实例

实例化一个变形虫,你需要按名称调用Amoeba类,并将其赋值给一个变量——就像你调用返回值的函数一样。实例化是一个高级词汇,意思是创建一个新实例,而实例对象是同义词。

添加一行代码来从你的Amoeba类创建一个新实例,并将其赋值给名为a1的变量:

class Amoeba(object):     def __init__(self): print('amoeba initialized')
a1 = Amoeba()

当你运行草图时,Python 会创建一个新的 Amoeba() 实例。这将自动调用 __init__() 方法。你可以使用 __init__() 方法来定义属性并为其赋值,稍后你会这样做。这个方法也可以包含其他指令来初始化变形虫,例如在这个例子中使用 print() 函数。当你运行草图时,控制台应该显示一条 amoeba initialized 消息。

向类添加属性

你可以把属性看作是属于对象的变量。就像一个变量一样,属性可以包含你喜欢的任何数据,包括数字、字符串、列表、字典,甚至其他对象。例如,Car 类可能有一个字符串属性来表示车型名称,还有一个整数属性来表示最高车速。

在你的 Amoeba 类中,你将添加三个属性来存储 x 坐标、y 坐标和直径的数值;当你实例化新的变形虫时,你将为这些属性赋值。语法类似于传递参数给函数:__init__() 方法的括号中包含了你对应的参数列表。

对代码做出以下更改,以便为每个新变形虫添加 xydiameter 值:

class Amoeba(object): def __init__(self**, x, y, diameter**): print('amoeba initialized')a1 = Amoeba(**400, 200, 100**)

__init__() 方法已经包含一个参数 self;这是必须的,并且总是第一个参数。self 参数提供对特定实例值的访问,例如变形虫 a1x 值为 400(但如何工作稍后再讲)。xydiameter 被作为第二、第三和第四个参数添加。我在 a1 行中添加了相应的实参。然而,请注意,我只提供了三个实参,self 参数没有提供任何值。图 10-3 描述了这些位置参数如何匹配,从 __init__() 方法中的第二个参数开始。

f10003

图 10-3:不要为 self 参数提供实参。

你也可以使用关键字参数(并为参数指定默认值),但在这个任务中我将坚持使用位置参数。

当你将值传递给 __init__() 方法时,它不会自动为你存储这些值。为此,你需要属性,它们就像是对象的变量。将 xydiameter 参数分配给新的属性。每个属性都以 self 为前缀,后跟一个点,再加上属性名:

class Amoeba(object): def __init__(self, x, y, diameter): print('amoeba initialized') self.x = x self.y = y self.d = diametera1 = Amoeba(400, 200, 100)

请注意,你将 diameter 分配给了 self.d。你的属性名称不必与参数名称相匹配。

在这一点上,我可以更详细地解释 self 参数。我提到过,self 是一个特定实例的引用。换句话说,self.d 值为 100 属于变形虫 a1。每个变形虫实例将拥有自己的一组 self.xself.yself.d 值。例如,我可能会添加另一个变形虫 a3,并赋予不同的值:

a3 = Amoeba(600, 250, 200)

这将在稍后你向模拟中添加多个变形虫时派上用场。图 10-4 提供了你的 Amoeba 类及其三个可能实例的概念图。

接下来,你将学习如何通过 a1 实例访问变形虫 a1xyd 值。你将使用这些值在显示窗口中绘制变形虫,类似于图 10-4 右上角所示的变形虫。

f10004

图 10-4:你的 Amoeba 类及其三个实例

访问属性

要访问属性,你使用 点表示法。对于 a1 实例,你可以通过 a1.xa1.ya1.d 分别访问 xyd 属性。这是实例名(a1)后跟一个点,再跟你想访问的属性名称。

要开始,向你的草图末尾添加以下代码,它绘制了一个圆形,表示变形虫 a1

. . .def setup(): size(800, 400) frameRate(120)def draw(): background('#004477') # cell membrane fill(0x880099FF) stroke('#FFFFFF') strokeWeight(3) circle(a1.x,  a1.y,  a1.d)

现在,显示窗口的宽度为 800 像素,高度为 400 像素。120 的高帧率将有助于平滑你稍后添加到变形虫中的抖动动画。细胞膜将变形虫的内部与外部环境分隔开,我在这里为它设置了一个白色的描边。填充颜色是半透明的浅蓝色。在 circle() 函数中的 x 坐标(第一个参数),Python 会检查 a1 实例的属性 self.x —— 在这个例子中,它的值为 400;y 坐标的参数值为 200,直径参数的值为 100。结果(图 10-5)是一个直径为 100 像素的圆形,位于显示窗口的中心。

f10005

图 10-5:直径为 100 像素的圆形(基础变形虫)

到目前为止,你已经学会了如何为 Amoeba 类添加参数,当你实例化变形虫时将其赋值给属性。除了这些参数外,你的类还可以包含具有预定义值的属性。

添加一个具有默认值的属性

想想汽车的类比。每辆车从生产线下来时油箱是空的。制造商可能在出售前加油,但油箱总是从空开始。为此,你决定为 Car 类添加一个属性——我们称之为 self.fuel。每个新车对象的默认值为 0,但它将在车辆的使用过程中变化。通过参数指定从 0 开始是多余的;相反,Car 类应该自动为你初始化 fuel 属性,默认为 0。

让我们回到变形虫的任务。每个变形虫都会包含一个预定义的红色填充的细胞核。要实现这一点,在 __init__() 方法的函数体内为一个名为 nucleus 的属性分配一个十六进制值(#FF0000)。你不需要在 __init__() 定义中添加另一个参数,因为你不需要额外的参数来指定红色填充:

 . . . self.x = x self.y = y self.d =  diameter self.nucleus = '#FF0000'. . .

现在,你创建的每个变形虫都有一个nucleus属性,值为#FF0000

在你的draw()函数中插入三行新代码,以便在细胞膜下渲染细胞核:

. . .def draw(): background('#004477') # nucleus fill(a1.nucleus) noStroke() circle(a1.x, a1.y, a1.d/2.5) # cell membrane. . .

新的代码行设置了填充色和描边,并通过使用circle()函数绘制细胞核,直径是细胞膜的 2.5 倍小(a1.d/2.5),并将其置于变形虫的中心。运行草图确认你看到的是紫褐色的细胞核;它实际上是红色的,但你透过淡蓝色的半透明膜看到它。

当你实例化变形虫时,并不设置细胞核的填充色,但这并不意味着你只能使用红色细胞核。你可以在创建变形虫后修改属性值。

修改属性值

许多属性保存的值会随着程序的运行而改变。为了回到汽车的比喻,考虑前面提到的fuel属性,它的值会随着油箱在满与空之间的波动不断变化。你可以通过实例使用相同的点语法直接修改任何属性的值。

插入一行代码来改变变形虫实例a1的细胞核填充色:

 . . . # nucleus a1.nucleus = '#00FF00' fill(a1.nucleus) . . .

这将nucleus属性设置为绿色,覆盖了默认的红色值。运行草图确认你能看到一个绿色的细胞核穿透半透明膜。

你也可以通过使用方法来修改属性,这部分我将在《为类添加方法》(第 216 页)中介绍。

使用字典来表示属性

请记住,属性可以包含任何你喜欢的内容——数字、字符串、列表、字典、对象等等。你将使用一个字典属性,该字典包含字符串(十六进制)和浮点值的混合,用于组织细胞核的属性。

将你的nucleus属性更改为一个字典,该字典包含细胞核填充色、x 坐标、y 坐标和直径的键值对。为了使每个变形虫的外观有所不同,可以随机化这些值:

class Amoeba(object): def __init__(self, x, y, diameter): print('amoeba initialized') self.x = x self.y = y self.d = diameter self.nucleus = { 'fill': ['#FF0000', '#FF9900', '#FFFF00', '#00FF00', '#0099FF'][int(random(5))], 'x': self.d * random(-0.15, 0.15), 'y': self.d * random(-0.15, 0.15), 'd': self.d / random(2.5, 4) }. . .

fill键与一个从五种颜色中随机选取的十六进制值配对。现在每个新变形虫的细胞核颜色是随机选择的(尽管你之后可能会明确地覆盖它)。xy键被赋予与细胞膜直径成比例的随机值;你将用这些值来定位细胞核在细胞膜的边界内,但不一定是中心位置。细胞核的直径(d)也与细胞膜成比例,并且每个实例的值是随机变化的。

更新你的draw()代码,以便处理这些更改:

. . .def draw(): background('#004477') # nucleus fill(a1.nucleus['fill']) noStroke() circle( a1.x + a1.nucleus['x'],  a1.y + a1.nucleus['y'],  a1.nucleus['d'] ) # cell membrane . . .

fill()circle()参数引用相关字典键,以样式和定位细胞核。

每次运行草图时,Processing 都会生成一个独特的变形虫。图 10-6 展示了四次运行的四个结果。当然,Processing 生成相同或相似的随机值组合是可能的(尽管不太可能),连续的结果可能看起来是一样的。

f10006

图 10-6:每个变形虫都是通过随机化的细胞核值生成的。

现在你已经设置了控制变形虫视觉外观的属性,下一步是添加方法来动画化它。

向类添加方法

你在类体内定义的函数被称为方法。用车的比喻来说,驾驶员可以通过使用方法来控制车辆,例如转向、加速和刹车。你还可以添加一个加油的方法。方法通常通过使用对象的属性来执行操作。例如,accelerate()refuel() 方法会分别减少或增加 fuel 属性的值。

你可以任意命名方法,只要遵循与函数相同的命名规则和约定。换句话说,只使用字母数字和下划线字符,使用驼峰命名法或下划线替代空格,等等。

你将为每一帧创建一个新方法来绘制变形虫。目前,代码中 draw() 部分的几行代码正在处理此操作。将细胞核和细胞膜的代码从 draw() 函数移到新创建的 display() 方法的主体中,确保你的缩进正确。在 display() 方法中,将每个 a1 前缀替换为 self

class Amoeba(object): . . . def display(self1): # nucleus fill(self.nucleus['fill']) noStroke() circle( self.x + self.nucleus['x'],           self.y + self.nucleus['y'],           self.nucleus['d'] ) # cell membrane fill(0x880099FF) stroke('#FFFFFF') strokeWeight(3) circle(self.x, self.y, self.d). . .def draw(): background('#004477')

定义中的 self 参数为你的 display() 方法提供了访问属性的能力,例如 self.nucleusself.xdisplay() 方法不接受任何参数,因此定义中没有其他参数。

调用方法

一旦你定义了一个方法,就可以像访问属性一样,使用相同的点符号调用该方法并执行方法体中的代码——也就是说,实例名称后跟方法名,并用点分隔。当然,方法像函数一样包含括号,有时也需要传递参数。

在你的 draw() 函数中添加一个 a1.display() 调用来渲染变形虫 a1

. . .def draw(): background('#004477') a1.display()

你在 display() 定义中没有参数(除了 self),所以方法调用不需要任何参数。运行草图以确认它产生与之前相同的结果(图 10-6)。

为了让变形虫摇晃,你将定义一个新方法,并在 Amoeba 类中调用它。此外,这个方法将接受一些参数。

创建一个摇晃的变形虫

变形虫像充满水的气球一样发生变形和波动。为了复制这种不完全圆形的形状,你将用 bezierVertex() 函数代替细胞膜的 circle() 函数。这与第二章绘制中国硬币时使用的代码相同,只是这里的控制点有些不规则。

图 10-7 展示了具有可视化顶点和控制点的变形虫轮廓。该形状并不完全圆形,但它是平滑的,没有明显的角度。对于平滑的曲线,顶点和两个控制点必须形成一条直线。

f10007

图 10-7:使用贝塞尔曲线绘制变形虫

为了实现摆动效果的动画,你需要调整每一帧中控制点的位置。为了避免可察觉的角度并保持曲线的圆滑外观,你将沿圆形路径移动控制点。图 10-8 展示了(从左到右)两个控制点完成一次旋转;每个控制点最终回到它开始的位置,准备无缝地重复运动。

注意,相对的控制点总是领先或落后于其对应点 180 度。当控制点接近顶点时,曲线变得更紧凑,但保持圆形。圆形轨迹保持从一个控制点到另一个控制点通过顶点的(虚拟)直线。

f10008

图 10-8:沿圆形路径移动控制点坐标

要编写这个效果,添加一个circlePoint()方法,用于计算每个圆形路径的周长上的点(这个方法是你在第九章定义的circlePoint()函数的改编):

class Amoeba(object): . . . def circlePoint(self, t, r): x = cos(t) * r y = sin(t) * r return [x, y] . . .

circlePoint()方法接受两个参数,一个是 theta(t)值,另一个是半径(r)。函数作用域的规则同样适用于方法,因此xy变量是circlePoint()方法的局部变量。

你可以通过类实例调用方法——例如,使用a1.circlePoint()调用circlePoint()方法。当然,你需要包含两个参数(用于tr)。你也可以通过在类内使用self前缀来调用方法——例如,self.circlePoint()。通过这种方式,你可以在display()函数内部调用circlePoint()方法,并使用返回的值来绘制摆动的变形虫。

display()块中添加一个circlePoint()方法调用,并用由bezierVertex()函数组成的代码替换circle()函数(用于细胞膜),绘制出一个由贝塞尔曲线组成的形状:

 . . . def display(self): . . . # cell membrane fill(0x880099FF) stroke('#FFFFFF') strokeWeight(3) r = self.d / 2.0 cpl = r * 0.55 cpx, cpy = self.circlePoint(frameCount/(r/2), r/8) xp, xm = self.x+cpx, self.x-cpx yp, ym = self.y+cpy, self.y-cpy beginShape() vertex( self.x, self.y-r # top vertex ) bezierVertex( xp+cpl, yp-r, xm+r, ym-cpl, self.x+r, self.y # right vertex ) bezierVertex( xp+r, yp+cpl, xm+cpl, ym+r, self.x, self.y+r # bottom vertex ) bezierVertex( xp-cpl, yp+r, xm-r, ym+cpl, self.x-r, self.y # left vertex ) bezierVertex( xp-r, yp-cpl, xm-cpl, ym-r, self.x, self.y-r # (back to) top vertex ) endShape(). . .

r变量表示变形虫的半径。cpl值是每个控制点到其顶点的距离;请记住,对于完全圆形的圆,这大约是圆半径的 55%(参见第二章,第 2-22 图)。circlePoint()方法通过使用基于递增的frameCount的 theta 值,计算cpxcpy变量的坐标;frameCount被除以变形虫半径的一半,以便较大的变形虫比小的变形虫摆动得更慢。第二个circlePoint()参数,即圆形路径的半径,也与变形虫半径成正比。其余的代码使用cplcpxcpy变量来绘制组成摆动变形虫的顶点和曲线。

运行草图以确认你有一个摆动的变形虫。

通过使用方法修改属性

你可以使用一种方法来修改一个或多个属性,作为直接通过点符号修改值的替代方案。以下是一个简短的示例;不需要将这段代码添加到你的草图中。

当你实例化变形虫 a1 时,你的 __init__() 方法会从一个预定义的五种颜色的列表中随机选择一个核填充色。你可以通过将另一个值赋给 a1.nucleus['fill'] 来改变这一点。或者,你可以定义一个新的方法来实现这一点:

class Amoeba(object):  . . . def styleNucleus(self, fill): self.nucleus['fill'] = fill  . . .

styleNucleus() 定义包含一个填充值的参数。在你实例化变形虫 a1 之后,你可以通过使用 a1.styleNucleus('#000000') 来设置核的填充色为黑色,而不是 a1.nucleus['fill'] = '#000000'。这看起来可能没什么用处,但考虑到你可以为核字典的 xyd 值添加额外的参数,一次性更改它们。你甚至可以添加额外的逻辑,例如 if 语句来检查直径值的大小,确保在应用之前它的大小是合适的:

 def styleNucleus(self, fill**, diameter**): self.nucleus['fill'] = fill if diameter > self.d/4 and diameter < self.d/2.5: self.nucleus['d'] = diameter

styleNucleus() 定义现在包含一个额外的参数,用于表示核的直径。但新的直径值仅在其大小合适时才会生效。if 语句将确保该方法忽略任何过小或过大的值,从而避免得到一个小核或一个超大、超出细胞膜的核。

在继续之前,简要回顾一下你在变形虫模拟中所处的阶段。你已经定义了一个 Amoeba 类,包含了改变每个实例外观的属性。你创建了一个变形虫 a1,但很快你将添加更多实例。你定义了一个 __init__() 方法来初始化属性。此外,你还定义了一个 display() 方法来绘制变形虫,并调用了另一个方法 circlePoint() 来使细胞膜产生波动。稍后,你会让变形虫在显示窗口中移动。不过,在此之前,你需要将你的显微镜草图拆分成两个文件。

将你的 Python 代码拆分成多个文件

在本书中,你完成了一系列相对较小的编程任务。将每个草图放在一个文件中处理还算可行,但随着你开始处理更复杂的程序,行数将会增加。你可能会将一个 俄罗斯方块 游戏压缩成几百行 Processing 代码,但开源的 Minecraft 类似游戏 Minetest 几乎有 60 万行(主要是)C++ 代码,而 Windows XP 包含大约 4500 万行源代码!

编程语言有多种机制来组织跨多个文件的项目。在 Python 中,你可以从文件中导入代码。每个被导入的 Python 文件被称为模块。在本节中,你将为 Amoeba 类创建一个单独的变形虫模块。

你需要考虑将程序合理划分为模块的最佳方式。例如,你可以将一组相关的函数归为一个模块。有时,将变量添加到一个专用的配置模块中也是很有用的,这样可以提供一个单独的位置来设置程序全局值。将一个或多个相关的类分组到一个模块中是组织代码的另一种有效方式。

在 Processing 编辑器中,每个标签代表一个模块。通过使用位于microscopic标签右侧的箭头(如图 10-9 所示)创建一个新的标签/模块。从弹出的菜单中选择New Tab,并将新文件命名为amoeba

f10009

图 10-9:点击用品红色高亮显示的箭头标签进行各种标签操作。

这个新文件/模块被创建在microscopic文件夹中,与主草图文件并列。Processing 会在amoeba文件名后添加.py,这是 Python 模块的标准文件扩展名。现在,amoeba.py模块应该会出现在microscopic标签旁边。

你可以通过标签切换在主草图和模块之间进行切换。切换到microscopic标签,选择所有关于Amoeba类的代码,剪切它,然后切换到amoeba.py标签并粘贴到那里(见图 10-10)。

f10010

图 10-10:amoeba.py标签包含了你的Amoeba类的代码。

现在切换回microscopic标签。剩下的就是从a1 = Amoeba(400, 200, 100)开始的所有内容。

要导入模块,使用import关键字。你的import行必须在任何实例化变形虫的代码之前。通常,import行放在文件的顶部,以避免顺序错误。下面是你microscopic标签中的完整代码:

from amoeba import Amoebaa1 = Amoeba(400, 200, 100)  def setup(): size(800, 400) frameRate(120)def draw(): background('#004477') a1.display()

from关键字指示 Python 打开amoeba模块。该模块的名称来自文件名amoeba.py,但省略了.py扩展名。接着是import,用来指定你想导入的类,这里是Amoeba。这种语法允许你根据需要从包含多个类定义的模块中选择性地导入类。现在,你可以像使用microscopic标签中的定义一样使用Amoeba类。

运行草图。它应该像往常一样运行,并在显示窗口的中心显示一个摇晃的变形虫。

你可以使用模块在项目之间共享代码。例如,你可以将变形虫模块复制到任何 Processing 项目文件夹中。然后,你只需导入它即可开始创建变形虫。你还可以将多个模块存储在一种被称为的文件夹结构中。

这个模块化系统使得编程更高效。除了减少主草图的行数外,你还隐藏了每个模块的内部工作原理,让程序员可以专注于更高层次的逻辑。例如,如果你为amoeba模块提供文档,说明如何实例化变形虫并使用方法,那么任何程序员都可以导入并使用它——在不查看amoeba.py代码的情况下创建变形虫。此外,模块化使得另一个程序员可以更容易地浏览你的项目代码并理解你的程序,因为它已经被划分为命名文件。

你的a1变形虫保持在固定位置,并随着时间推移而摇晃。下一步是让它在显示窗口中移动。

使用向量编程运动

你将使用向量来编程变形虫的运动。然而,这些向量不是用于可缩放图形的向量,而是欧几里得向量。欧几里得向量(也称为几何向量空间向量)表示一个既有大小又有方向的量。你将使用向量来模拟推动变形虫的力。

在图 10-11 中,变形虫从 A 点移动到 B 点,总共推动了 4 个单位的距离。这个距离代表了一个大小;大小描述了一个力的强度。一个更大大小的力可能会将同样的变形虫推移 20 个单位。然而,问题在于——大小并没有指示力应用的方向;你只能通过视觉上获得的信息知道,运动是向右移动了 4 个单位。

f10011

图 10-11:4 个单位的大小

量值是一个标量值。它是一个可以用单一数值(如浮点数或整数)描述的单一量。例如,数字 4、1.5、42 和一百万都是标量。

向量由多个标量描述。换句话说,它可以包含多个浮点数或整数值。图 10-12 展示了一个标记为v的向量,它是带箭头的直线。v的长度是它的大小;斜率和箭头指示它的特定方向。

f10012

图 10-12:向量v向右延伸 4 个单位,向上延伸 3 个单位。

每个向量都有一个 x 和一个 y 分量,因此你可以将这个向量表示为v = (4, 3)。它描述了一个将变形虫移动到一个新位置的力,这个新位置比原位置向右 4 个单位,向上 3 个单位。你用粗体字表示向量,但在某些粗体不方便使用的情况下(例如,手写公式),也常常在v上方画一个小箭头。

图 10-12 中的水平和垂直测量线与v形成一个直角三角形,v是其中的斜边。通过这个三角形,你可以使用勾股定理来计算向量的大小。该定理指出,斜边的平方等于另外两边的平方和。

如果你将 4 的平方(邻边)加上 3 的平方(对边),你会得到 25,这是斜边的平方。25 的平方根是 5,即斜边的长度,也是v的大小。但你不需要担心进行这样的计算。Processing 提供了一个内建的PVector类,专门用于处理向量,其中包括一个mag()方法来计算大小。

你将调整你的变形虫草图,使其能够使用PVector类进行运动。在展示如何使用向量让变形虫移动的同时,我还会概述各种PVector方法的工作原理,揭示其背后的数学原理。

PVector

PVector是 Processing 中的一个内建类,用于处理欧几里得向量。你可以在任何草图中使用它——无需import语句。PVector可以处理二维和三维向量,但我们这里仅使用二维向量。

要创建一个新的二维向量,PVector()类需要提供 x 和 y 参数。例如,这行代码定义了在图 10-12 中所示的向量:

v = PVector(4, 3)

v实例是一个新的向量,水平延伸 4 个单位,垂直延伸 3 个单位。不过,你应该将3改为-3,以适应 Processing 的坐标系统(在这个系统中,y 值随着向上移动而减少)。

向量可以指向任何方向,无论是负方向还是正方向,但其大小始终是一个正值。使用mag()方法来计算任何PVector实例的大小;例如:

magnitude = v.mag()print(magnitude) # displays 5.0

你知道mag()方法必须调用基于毕达哥拉斯定理的预写代码。它返回一个浮动值 5.0,确认了我们在上一节中的计算结果。

使用 PVector 移动阿米巴

你将创建一个PVector实例来让阿米巴a1在显示窗口中移动。在第六章中,你编写了类似的程序——一个 DVD 屏保——通过指示 Processing 在每一帧水平和垂直地移动 DVD logo,达成平滑的对角线运动。这里的方法类似,但你将使用PVector类。你会发现,基于向量的方法在模拟运动和力方面更为高效。

切换到amoeba.py标签,并在__init__()方法中添加一个新的propulsion向量:

class Amoeba(object): def __init__(self, x, y, diameter**, xspeed, yspeed**): . . . self.propulsion = PVector(xspeed, yspeed)

propulsion 向量通过额外的xspeedyspeed两个参数初始化,这将决定每一帧中阿米巴水平和垂直推进的像素数。与 DVD 屏保任务相比,这里你将xspeedyspeed变量合并为一个名为propulsion的单一向量。

现在切换到microscopic标签。使用第四个和第五个Amoeba()参数将推进向量的 x 和 y 分量分别设置为3-1。使用draw()函数根据这些值递增阿米巴的 x 和 y 属性:

. . .a1 = Amoeba(400, 200, 100**, 3, -1**). . .def draw(): background('#004477') a1.x += a1.propulsion.x a1.y += a1.propulsion.y a1.display()

每一帧,阿米巴a1的 x 值增加 3 个像素;与此同时,它的 y 值减少 1。在默认的 Processing 坐标系统中,y 值减小表示阿米巴向上移动。如果运行该草图,阿米巴应沿对角线快速移动,起始于显示窗口的中心,并很快从右上角下方退出。

你还可以使用PVector实例来存储阿米巴的 x 和 y 坐标。实际上,你可以使用PVector来存储任何 x-y 坐标对;毕竟,它是一个用于存储两个(或三个)数字的对象,同时还包括许多方便的向量运算方法。切换到amoeba.py标签;将self.xself.y属性替换为一个名为self.location的新向量:

class Amoeba(object):     def __init__(self, x, y, diameter): print('amoeba initialized') self.location = PVector(x, y) . . .

变形虫的位置现在也成为一个 PVector 实例,尽管它描述的是显示窗口中的一个点,而不是速度或力。但你还不能重新运行草图。首先,你需要更新其余的 amoeba.py 文件,使其与新的位置属性兼容。

你的 Amoeba 类中有多个 self.xself.y 的引用,你需要确保将它们全部替换为 self.location.xself.location.y。最简单的方法是使用查找和替换操作。在 Processing 菜单栏中,选择 编辑查找 来访问 查找 工具(图 10-13)。在 查找 字段中输入 self.x,在 替换为 字段中输入 self.location.x。点击 全部替换 按钮应用更改。这里的复选框设置不会影响结果。完成后,对 self.y 做同样的操作,替换为 self.location.y

f10013

图 10-13:处理查找(和替换)工具

现在,将你在 microscopic 标签页中的 a1.xa1.y 分别更改为 a1.location.xa1.location.y

 . . . a1.**location**.x += a1.propulsion.x a1.**location**.y += a1.propulsion.y . . .

你将 x 分量和 y 分量分别添加到不同的行中。然而,有一种更高效的方法来实现这一点,即使用 PVector 加法。

向量加法

+ 运算符用于添加浮点数或整数。此外,它还作为字符串操作数的连接运算符。PVector 类也被编程为与 + 运算符一起使用。你可以将一个 PVector 实例添加到另一个实例中,从而得到一个表示两个向量和的新向量。通过扩展,+= 作为一个增量赋值运算符,表示运算符左侧的向量操作数等于它自身加上右侧的操作数。

用一行代码替换你的 a1.x += propulsion.xa1.y += propulsion.y,通过添加 PVector 实例来代替单独的分量:

. . .def draw(): background('#004477') a1.location += a1.propulsion a1.display()

每次调用 draw() 函数(每一帧)时,变形虫的位置都会根据推进力向量递增。如果你运行草图,变形虫会沿着和之前相同的轨迹移动,每一帧水平移动 3 像素,垂直向上移动 1 像素,最终从显示窗口的右上角下方退出。

让我们为模拟添加一个新力。你将模拟一个斜向流动的水流;它辅助变形虫的主要运动,朝东北方向流动。正如维基百科(en.wikipedia.org/wiki/Current_(fluid))定义的那样,“流体中的水流是该流体中流动的大小和方向。”显然,这需要通过向量来模拟。

microscopic 标签页中添加一个名为 current 的新 PVector。在每一帧中,通过 draw() 函数将该向量添加到你的当前位置:

. . .
current = PVector(1, -2). . .def draw(): background('#004477') a1.location += a1.propulsion a1.location += current a1.display()

推进向量大约与水平线成 18 度角,推力主要向右而非向上。当前向量大约与水平线成 63 度角,推力主要向上而非向右(见 图 10-14)。这种组合使变形虫以一个大约 36 度的角度向前移动,比之前的方式快。如果你运行草图,变形虫应该会从显示窗口的顶部边缘退出(之前它是从右边缘退出的)。

f10014

图 10-14:变形虫每帧移动 4 像素的水平距离和 3 像素的垂直距离。

向量加法通过将一个向量的 x 分量与另一个向量的 x 分量相加,y 分量也同样如此。在这个例子中,x 分量相加 (3 + 1) 等于 4,y 分量相加 (–1 + –2) 等于 –3。无论加法的顺序如何,结果总是相同的。例如,(3, –1) + (1, –2) 和 (1, –2) + (3, –1) 的结果都是相同的,结果向量在两种情况下都是 (4, –3)。这使得向量加法成为一个 交换 操作,因为改变操作数的顺序不会改变结果。

你可以尝试不同的电流值,看看会发生什么。一个电流向量 (–3, 1) 完全抵消了推进向量,变形虫将停留在显示窗口的中心。而电流向量 (–3.5, 1) 将压倒推进向量的 x 分量,并与 y 分量完全匹配,导致变形虫缓慢而直接地向左移动。

这个系统的妙处在于你可以根据需要将任意数量的力作用于物体的位置。例如,你可能会包括一个风的向量,一个重力的向量,等等。

向量相减

在数学中,减法运算的结果称为 。例如,当你从 6 中减去 4 时,剩下的差是 2。同样,当你从一个向量中减去另一个向量时,结果向量是两者之间的差。

你可以将向量相减想象成这样:首先将两个向量的尾部对齐;在每个向量的头部之间画一条线;这条新线就是差向量。在 图 10-15 中,你从 a 中减去了 b;其差向量(深蓝色向量 c)是 (–2, –1)。

f10015

图 10-15:向量 c 等于 (–2, –1)。

向量相减的过程与向量加法类似,但你不再是将每个向量的 x 分量(x 分量)和 y 分量(y 分量)相加,而是进行相减。然而,注意减法是 非交换 的。这意味着,改变操作数的顺序会改变结果。例如,如果你从 b 中减去 a,你会得到 (2, 1) 而不是 (–2, –1)。这使得向量 c 指向相反的方向,交换了它的头和尾。

你可以通过使用 运算符来减去 PVector 实例。以下是一个示例:

print(current - a1.propulsion)

如果你的当前向量为 (1, –2),这将在控制台打印 [-2.0, -1.0, 0.0]。Processing 将一个 PVector 实例打印为三个浮动点数值的列表,分别表示向量的 x、y 和 z 分量。z 值始终为 0,除非你在处理三维向量。

你已经将推进向量和当前向量加到变形虫的位置上,以使其在显示窗口中移动。现在,你将应用向量减法的知识,使变形虫朝鼠标指针移动。你将创建一个新的 PVector 实例,名为 pointer,用来存储鼠标指针的 x-y 坐标。你将从 pointer 中减去 location(它包含变形虫的 x-y 坐标)来找到差异向量(图 10-16),你将使用这个向量来重新定向变形虫。

f10016

图 10-16:差异向量等于 pointerlocation

确保你的当前向量设置为 (1, –2)。添加一个新的 PVector,命名为 pointer,并创建一个差异变量,它等于指针减去变形虫的位置(图 10-16 中所示的差异向量)。

. . .current = PVector(**1**, **-2**). . .def draw(): background('#004477') pointer = PVector(mouseX, mouseY) difference = pointer - a1.location a1.location += difference . . .

mouseXmouseY 是 Processing 系统变量,保存着鼠标指针的 x 和 y 坐标。但需要注意的是,Processing 只有在鼠标指针移动到显示窗口前后,才会开始跟踪鼠标位置;在此之前,mouseXmouseY 都会返回默认值 0。

如果你运行这个草图,变形虫将附着在鼠标指针上。这是因为变形虫会在一次“跳跃”中到达指针位置。相反,你希望变形虫朝着指针“游泳”,在多个帧中逐步前进。

限制向量大小

PVector 类提供了 limit() 方法来限制任何向量的大小,这不会影响方向。它需要一个标量(整数或浮动点数)作为参数,表示最大大小。

你将使用差异向量通过将其添加到推进向量中来引导变形虫朝向鼠标指针。你将限制推进向量的大小为 3(图 10-17),足以在变形虫直接游向当前方向时,稍微克服其大小为 2.24 的作用力。

f10017

图 10-17:推进向量的大小限制为 3。

draw() 函数中做以下插入/修改,以引导和推进变形虫朝着鼠标指针前进:

. . .def draw(): . . . 1 **#**a1.location += difference 2 **a1.propulsion += difference.limit(0.03)** 3 a1.location += a1.propulsion**.limit(3)** a1.location += current a1.display()

首先,将现有的 a1.location += difference 行 1 注释掉或删除。limit() 方法将 difference 向量限制为大小为 0.03。这个微小的值将在每一帧中被添加到推进向量中——这种效果迅速累积——将变形虫逐步引导朝向鼠标指针。但即使变形虫直接朝指针前进,推进向量的大小也不会超过 3。

运行草图,将鼠标指针移动到显示窗口的左下角附近。变形虫将会漂移到视野之外。但稍等一会儿,它会慢慢朝着那个角落移动;当它到达指针时,会稍微超越它,然后转身返回时再超越一次。它会不断超越指针,因为它试图尽快到达目标。现在将指针移到右下角。在水流的帮助下,变形虫迅速到达屏幕的另一侧,但由于其较高的速度,它会剧烈地超越目标。

很快,你将会向模拟中添加多个变形虫。为了让它们以不同的速度移动,向Amoeba类中添加一个最大推进力的属性:

class Amoeba(object): def __init__(self, x, y, diameter, xspeed, yspeed): . . . self.maxpropulsion = self.propulsion.mag()

这个属性将根据你提供的xspeedyspeed参数来限制变形虫推进向量的大小/力度。修改你在微观标签中的代码,使其能够使用maxpropulsion属性,替换两个limit()方法的参数。另外,调整xspeedyspeed和当前向量的值,将它们缩小一个因子 10:

. . .a1 = Amoeba(400, 200, 100, **0.3**, **-0.1**)current = PVector(**0.1**, **-0.2**). . .def draw():  . . . a1.propulsion += difference.limit(**a1.maxpropulsion/100**) a1.location += a1.propulsion.limit(**a1.maxpropulsion**) . . .

减少的推进力和当前值使得模拟变得更慢,因此变形虫的移动更加平稳和可控。变形虫不再猛烈超越目标,但它仍然会在指针周围做小的轨道运动。差值向量的限制现在与变形虫的最大推进力成比例,因此较快的变形虫具有额外的操控力来应对其更高的速度。

执行其他向量操作

向量和PVector类还有更多内容,但这些内容是本书中涉及的所有内容。可以将你所学的视为该主题的初步入门。PVector类还可以处理向量乘法、除法、归一化、三维向量等操作。向量对于编程涉及物理的任何内容都非常有用,比如视频游戏,而且你很可能会在你的创意编程冒险中再次遇到它们。

向模拟中添加多个变形虫

你已经有了一个工作的变形虫模块,但你目前只在处理一个变形虫实例a1,因此下一步是创建一个变形虫群落。你可以从同一个类创建任意数量的实例。在这一部分中,你将使用Amoeba类在同一个显示窗口中生成八个变形虫。每个变形虫的大小都不同,且它们将从不同的 x-y 坐标开始。回想一下,每个变形虫实例包含一个随机化的细胞核值字典,因此细胞核也会有所不同。

添加变形虫的一种(相当手动)方法是定义额外的实例,并使用个性化的变量名和明确区分的参数。考虑以下三个新的变形虫:

a1 = Amoeba(400, 200, 100, 0.3, -0.1)
sam = Amoeba(643, 105, 56, 0.4, -0.4)
bob = Amoeba(295, 341, 108, -0.3, -0.1)
lee = Amoeba(97, 182, 198, -0.1, 0.2)
. . .

你可以通过这种方式不断添加变形虫,但这种方法也有其缺点。首先,你需要记得在draw()函数的主体中调用每个display()方法来渲染每个变形虫:

def draw():  . . . a1.display() sam.display() bob.display() lee.display() . . .

这将显示samboblee静止不动;要让这些变形虫动起来,draw()函数还需要更多代码。如果你处理的是大约 5 只变形虫,这样还算高效,更别说 100 只了。

个性化的变形虫名字很可爱,但对这个程序来说并不重要。相反,你将把变形虫存储在一个列表中。你可以方便地使用循环来生成任意数量的变形虫列表。然后,你可以通过另一个循环调用每只变形虫的display()方法(以及移动它的代码)。

用一个空的amoebas列表和一个循环来填充它,替换你显微镜代码顶部的a1行:

from amoeba import Amoeba
amoebas = []for i in range(8): diameter = random(50, 200) speed = 1000 / (diameter * 50) x, y = random(800), random(400) amoebas.append(Amoeba(x, y, diameter, speed, speed)). . .

每次for循环迭代时,Python 都会创建一个新的Amoeba()实例。Amoeba()的参数是随机的,用来改变每个实例的 x 坐标、y 坐标和直径。speed值是基于diameter的——因此,较大的变形虫移动得较慢(记得propulsionmaxpropulsion属性是从xspeedyspeed参数派生的)。append()方法将新的变形虫实例添加到amoebas列表中。变形虫没有像samboblee这样的名字,但你可以通过索引amoebas[0]amoebas[1]等来引用它们。

你必须向draw()函数添加一个for循环来渲染完整的变形虫列表。以下是你修改后的代码:

. . .def draw(): background('#004477') pointer = PVector(mouseX, mouseY) for a in amoebas: difference = pointer - a.location a.propulsion += difference.limit(a.maxpropulsion/100) a.location += a.propulsion.limit(a.maxpropulsion) a.location += current a.display()

for循环遍历整个amoebas列表。对于每个变形虫,它会计算一个更新的位置,然后使用它的display()方法来渲染该变形虫。

较大、较慢的变形虫可能会漂移出显示窗口,被水流压倒,永远无法再见到。为了避免这个问题,添加代码以实现环绕边缘——这样,如果变形虫退出显示窗口,它将重新出现在对面边缘,保持其速度和轨迹:

 . . . for a in amoebas: . . . r = a.d / 2 if a.location.x - r > width:    a.location.x = 0 - r if a.location.x + r < 0:        a.location.x = width + r if a.location.y - r > height:   a.location.y = 0 - r if a.location.y + r < 0:        a.location.y = height + r

四个if语句检查显示窗口的每个边缘。为了确保变形虫在重新出现在对面边缘之前已经完全离开显示窗口,有必要将半径(变量r)纳入条件中。同样,每个对应的目标位置也偏移了r,以防止变形虫在对面边缘中途重新出现。如果你想看看其他情况,可以将r设置为 0。

每次运行草图时,你都会得到不同的变形虫组合。它们都会朝你的鼠标指针聚集(尽管水流压倒了一些大而慢的变形虫),在过程中相互重叠。图 10-18 显示了一个例子,里面有八只变形虫。

要添加或删除变形虫,你可以调整第一个循环中range()函数的参数,draw()函数中的循环会动态适应。如果你的计算机似乎有些吃力,可以减少变形虫的数量。

f10018

图 10-18:一个显示窗口,里面有八只变形虫朝向鼠标指针移动

挑战#10:碰撞检测

变形虫可能会相互重叠。为了防止这种情况发生,必须先检测重叠发生的位置。然后,你可以应用向量力将任何发生碰撞的变形虫推开。

变形虫大致是圆形的,所以圆形-圆形碰撞检测算法在这里非常适用。要理解圆形-圆形碰撞检测是如何工作的,可以参考图 10-19。左边的一对圆形没有发生碰撞;右边的是发生了碰撞的圆形对。对于没有发生碰撞的圆形,它们的圆心之间的距离大于两圆半径之和(r1 和 r2)。相反,当圆形发生碰撞时,它们的圆心之间的距离小于两圆半径之和。

f10019

图 10-19:圆形碰撞检测

在 Processing 中测试碰撞时,你需要检查amoebas列表中的每一个变形虫与其他所有变形虫之间的碰撞。为此,你需要在a in amoebas循环中添加另一个for循环:

 . . . for a in amoebas: . . . for b in amoebas: if a is b: continue # your solution goes here

你不想检查一个变形虫是否与自己发生碰撞。在循环的顶部,有一个if a is b的测试。is操作符比较其两侧的对象,以确定它们是否指向同一个实例;如果ab是同一个实例,这个条件会被评估为Truecontinue语句终止当前循环的迭代,并从下一次循环的开始处重新开始,因此你的“解决方案”代码会被跳过。

想想如何利用图 10-19 中显示的距离向量,将发生碰撞的变形虫推开。你能否添加(或减去)一部分距离向量,将变形虫推向与碰撞方向相反的方向?

如果你需要帮助,可以访问解决方案:github.com/tabreturn/processing.py-book/tree/master/chapter-10-object-oriented_programming_and_pvector/

总结

在本章中,你学习了如何使用面向对象编程在 Python 中建模现实世界中的对象。你定义了一个新的Amoeba类,并为其添加了属性和方法。类作为对象模板,你可以从中创建无数个实例。将相关的变量(属性)和函数(方法)组织到类中,可以帮助你更高效地组织代码。这对于编写更大、更复杂的项目特别有效。

你还学习了如何将类(和其他代码)分离到不同的 Python 文件中,这些文件被称为模块,并且如何使用这些模块在项目之间共享代码,或者在同一项目中的文件间作为可重用的组件。记住,模块可以减少主草图的行数,让你能够专注于更高层次的逻辑。

本章还介绍了 Processing 内置的PVector类,用于处理欧几里得向量。欧几里得向量描述了一个既有大小又有方向的量,但你也可以用向量来存储某个位置(作为 x-y 坐标)。在本章中,你使用了向量来模拟力并控制显示窗口中各个对象的位置。

在下一章中,你将学习如何在 Processing 中处理鼠标和键盘交互。我在本章中已经提到过mouseXmouseY系统变量。然而,通过捕捉鼠标点击和按键事件,你可以做更多的事情,解锁与 Processing 草图互动的激动人心的新方式。

第十一章:鼠标和键盘交互

在本章中,你将学习如何编程互动草图,响应鼠标和键盘输入。你可以以有趣且实用的方式结合这些输入设备。例如,许多电脑游戏使用组合键来控制玩家移动,鼠标用于瞄准。在这里,你将编程草图,使用鼠标进行绘画并从工具调色板中选择项目。你还将添加快捷键,通过键盘激活工具。

本章介绍了你可以用来监控鼠标点击和键盘按键的系统变量。你还将学习到事件函数,这些函数会在特定类型的鼠标或键盘事件发生时执行。

你将完成的第一个任务是一个简单的涂鸦程序。第二个任务是一个更复杂的画图应用,它包括一个用于选择颜色和画笔的工具调色板。当你编程互动式草图时,你需要能够根据用户输入更改显示窗口的内容,因此这两个程序都设置为动画草图。

鼠标交互

你可以使用鼠标输入执行点击操作。你还可以编程手势类型的动作,结合鼠标移动和点击,例如拖放或平移。大多数鼠标有三个按钮——一个左键,一个右键,还有一个可以点击的滚轮,滚轮也可以作为中间按钮使用。

鼠标变量

Processing 的mouseXmouseY变量存储了你鼠标指针在显示窗口中的水平和垂直位置。Processing 还提供了系统变量pmouseXpmouseY,它们包含了上一帧的鼠标坐标。还有一个mousePressed变量,它在鼠标按钮被按下时会被设置为True

本章的第一个任务专注于 Processing 的鼠标变量,用于监控鼠标指针的位置并检测鼠标按钮是否被按下。你将编程一个简单的草图来制作划痕艺术。一张划痕艺术纸上覆盖着彩虹色的混合色彩,接着涂上一层黑色(图 11-1)。"刮痕者"使用塑料或木制触控笔在黑色表面上刻划线条,揭示下面的颜色。

f11001

图 11-1:构成划痕艺术纸张的一层层材料(左),用触控笔涂鸦(右)

你可以购买现成的划痕纸,或者自己制作,但像素是便宜且可重复使用的!

创建一个新文件并将其保存为scratch_art。添加以下代码,让你的鼠标绘制一串白色圆圈:

def setup(): size(800, 400) frameRate(20) background('#000000') stroke('#FFFFFF')def draw(): circle(mouseX, mouseY, 15)

每一帧,Processing 都会根据 mouseXmouseY 值绘制一个新圆。每帧都会获取这些坐标,并且帧率相对较低(20 fps)。draw() 块中没有 background() 函数,因此每个绘制的圆形都会持续存在,直到你关闭显示窗口。如果你慢慢移动鼠标,圆形会形成一条实心的白线;而当你快速移动鼠标时,线条中会留下一些明显的间隙(图 11-2)。显示窗口的左上角总是会有一个圆形,因为鼠标的第一个 x-y 坐标对等于 (0, 0)。

f11002

图 11-2:你移动鼠标的速度越快,线条中的间隙就越大。

你可以增加帧率以便更好地填充线条,但如果你足够快地移动鼠标,仍然会出现间隙。为了确保线条连续,可以用绘制线条的代码替换circle()函数:

. . .def draw(): strokeWeight(15) line(mouseX, mouseY, pmouseX, pmouseY)

strokeWeight() 的参数 15 与之前的圆形直径相匹配。line() 函数绘制从当前帧到前一帧的鼠标坐标之间的线条。

运行草图。第一个 line() 函数将从左上角 (0, 0) 延伸到鼠标指针首次进入显示窗口的位置。在 图 11-3 中,我的鼠标从左边缘进入显示窗口(我正从左向右画波浪)。

f11003

图 11-3:使用 line() 函数绘制每一帧的连续线条

为了打开和关闭“画笔”,插入一个 if 语句,当左键按下时激活 line() 函数:

. . .def draw(): strokeWeight(15) if mousePressed and mouseButton == LEFT: line(mouseX, mouseY, pmouseX, pmouseY)

mousePressed 变量保存一个布尔值,当任意鼠标按钮被按下时,该值为 TruemouseButton 变量根据按下的按钮为 LEFTRIGHTCENTER,若没有按下任何按钮,则为 0

运行草图,确认在按住左键时,Processing 绘制的是一条白色的线。

为了实现彩虹色的涂鸦艺术效果,插入代码使得笔触颜色基于鼠标指针的位置。水平方向控制色相,垂直方向控制饱和度:

. . .def draw(): colorMode(HSB, 360, 100, 100) h = mouseX * 360.0 / width s = mouseY * 100.0 / height b = 100 stroke(h, s, b) . . .

在这个例子中,你将颜色模式设置为 HSB(色相、饱和度、亮度)。h 变量分配一个 0 到 360 之间的色相值;s 变量分配一个 0 到 100 之间的饱和度值。hs 的值基于鼠标指针相对于显示窗口宽度和高度的位置。颜色的亮度值始终为 100%。

运行草图,测试完成的涂鸦艺术程序(图 11-4)。

f11004

图 11-4:在涂鸦艺术程序中涂鸦

现在你已经了解了 Processing 的鼠标变量,在接下来的任务中,你将学习鼠标事件函数。

鼠标事件

Processing 提供了一些鼠标事件函数,每当发生特定的鼠标事件时,它们就会被执行。这些函数包括 mouseClicked()mouseDragged()mouseMoved()mousePressed()mouseReleased()mouseWheel()。你可以在事件函数块中添加代码,当事件函数被触发时,该代码就会执行。为了说明这一点,我将通过一个使用 mousePressed 变量的例子与另一个使用 mousePressed() 事件函数的例子进行对比。

以下代码使用 mousePressed 系统变量,通过按下鼠标按钮将背景颜色从红色切换为蓝色:

def draw(): background('#FF0000') # red if mousePressed: background('#0000FF') # blue

当用户按住鼠标按钮时,背景颜色为蓝色;否则,背景颜色为红色。下一个例子使用一个鼠标事件——mousePressed() 函数——来执行类似的操作:

def draw(): background('#FF0000') # red def mousePressed(): background('#0000FF') # blue

每次按下鼠标按钮时,mousePressed() 函数会执行蓝色背景的代码,显示窗口会短暂地闪烁为蓝色(仅一帧)。无论你按住鼠标按钮多长时间,它都会立即返回红色。这是因为事件函数每次事件发生时只执行一次;换句话说,直到你松开并重新按下鼠标按钮,背景才会再次闪烁为蓝色。

创建一个绘图应用

在下一个练习中,你将编写一个基础的绘画应用程序,其中包括一个用于选择颜色样本和其他选项的工具面板。你将使用 mousePressed()mouseReleased()mouseWheel() 函数。

在 图 11-5 中,右侧的大块深蓝色区域是你的绘图画布;工具面板位于左边缘。按住左键可以进行绘制。

f11005

图 11-5:带有(画得很糟的)Python 标志的绘画应用

首先,创建一个新的草图并将其保存为 paint_app。你将使用由 Marc André “Mieps” Misman 创建的 Ernest 字体来标记工具面板中的按钮。你可以从本书的 GitHub 页面下载这个字体:

  1. 打开你的浏览器,访问 github.com/tabreturn/processing.py-book/

  2. 导航到 chapter-11-mouse_and_keyboard_interaction

  3. 下载 Ernest.ttf 文件。

  4. 在你的草图文件夹中创建一个新的 data 子文件夹,并将 Ernest.ttf 文件放入其中。

添加以下代码来设置你的草图。这段代码定义了显示窗口的大小、背景颜色、字体以及绘图应用的全局变量:

def setup(): size(600, 600) background('#004477') ernest = createFont('Ernest.ttf', 20) textFont(ernest)
swatches = ['#FF0000', '#FF9900', '#FFFF00',             '#00FF00', '#0099FF', '#6633FF']
brushcolor = swatches[2]
brushshape = ROUND
brushsize = 3
painting = Falsepaintmode = 'free'
palette = 60

你将使用全局变量(如 swatchesbrushcolor 等)来调整和监控画笔的状态。默认的画笔颜色设为黄色。稍后,你将使用 palette 变量来设置工具面板的宽度。此时你还没有添加任何视觉元素,因此如果运行草图,你看到的将只是一个简单的蓝色显示窗口。

使用 loop()noLoop() 函数控制绘制循环

你将通过使用鼠标事件来控制draw()函数的行为。当按下左键时,draw()函数将进入循环;一旦释放左键,循环停止,这是一种方便的方式来控制绘图应用的工作方式。当然,draw()函数默认是循环的,因此你需要使用loop()noLoop()函数来控制这种行为。

noLoop()函数停止 Processing 持续执行draw()块中的代码。loop()函数重新激活标准的draw()函数行为,如果你只需要执行一次draw()代码,可以使用redraw()函数。

首先,在setup()块中添加一个noLoop()函数,然后在draw()函数中打印帧计数:

def setup(): . . . noLoop(). . .def draw(): print(frameCount)

如果你运行草图,控制台应该只显示一个1,确认draw()函数只运行了一次。

现在在draw()函数中添加代码,使鼠标在显示窗口中绘制线条,并添加两个鼠标事件来启动和停止绘画流程:

. . .def draw(): print(frameCount) global painting, paintmode 1 if paintmode == 'free': 2 if painting: stroke(brushcolor) strokeCap(brushshape) strokeWeight(brushsize) 3 line(mouseX, mouseY, pmouseX, pmouseY) 4 elif frameCount > 1: painting = True5 def mousePressed(): # start painting if mouseButton == LEFT: loop()6 def mouseReleased(): # stop painting if mouseButton == LEFT: global painting painting = False noLoop()

在模拟过程中,仔细阅读这段代码,关注painting变量为TrueFalse时的情况,以及draw()函数何时会持续运行。草图开始时,painting变量被设置为False;此时draw()函数并没有进入循环。当你按下左键 5 时,loop()函数指示 Processing 重新开始循环draw()函数;当你释放按钮 6 时,noLoop()函数再次停止绘制行为。paintmode变量默认设置为free 1,因此 Python 会检查你当前是否在绘画 2。你稍后会添加其他绘画模式。如果painting等于True,Processing 会在当前帧的鼠标坐标和上一帧的坐标之间绘制一条线 3;如果不是,它会检查帧计数是否已经超过1 4,才会将painting变量设置为Trueif 2 和elif 4 步骤是必要的,以避免在停止和继续绘画(释放左键、移动鼠标,然后再次按下按钮)时绘制直线,而frameCount > 1则阻止 Processing 在从左上角到第一次开始绘画的地方绘制一条线。在图 11-6 中,左侧截图展示了如果省略这些语句时发生的情况。

f11006

图 11-6:Processing 在停止和开始绘画的点之间绘制直线(左),以及你版本的程序(右)

运行草图,绘制几个圆圈以测试代码是否正常工作。观察控制台并注意,帧计数只有在你按住左键时才会增加。

添加可选颜色样本

工具面板将包括六个颜色选色板,你可以用它们来更改画笔颜色。将以下代码添加到 draw() 块的底部,以在显示窗口的左侧渲染一个黑色面板,并在其中根据 swatches 列表绘制六个颜色选色板:

. . .def draw(): . . . # black panel noStroke() fill('#000000') rect(0, 0, palette, height) # color swatches for i, swatch in enumerate(swatches): sx = int(i%2) * palette/2 sy = int(i/2) * palette/2 fill(swatch) square(sx, sy, palette/2). . .

for 循环遍历 swatches 列表,绘制一个由不同颜色填充的方格网格。程序会在画笔描边之后绘制面板(和选色板元素),以防止在选择时出现不需要的描边覆盖在选色板上。

如果用户点击了某个颜色选色板,你必须将该颜色赋值给 brushcolor 变量;为此,向 mousePressed() 函数中添加代码:

. . .def mousePressed(): . . . # swatch select if mouseButton == LEFT and mouseX < palette and mouseY < 90: global brushcolor brushcolor = get(mouseX, mouseY)

if 语句测试鼠标左键单击,并检查鼠标指针是否位于颜色选色板的某个位置。get() 函数返回鼠标指针下方像素的颜色,并将其赋值给 brushcolor 变量。你添加一行 global 代码来覆盖全局作用域中的 brushcolor 变量,该变量在 draw() 函数中用于应用画笔颜色的描边。

运行草图。你现在可以选择颜色进行绘画(图 11-7)。

f11007

图 11-7:点击工具面板中的选色板以更改画笔颜色。

接下来,你将添加一个调整画笔大小的功能,将其映射到滚轮上。

使用滚轮调整画笔大小

mouseWheel() 事件函数用于在鼠标滚轮移动时执行代码。此外,你还可以使用它来根据滚轮的旋转方向获取正值或负值。然而,正负值的旋转方向取决于系统配置。触控板滚动也应该适用,通常通过两指拖动来实现。

在代码的最底部添加一个 mouseWheel() 函数:

. . .def mouseWheel(e): print(e)

mouseWheel() 函数括号中的 e 作为一个变量,用于接收事件的详细信息。你可以将其命名为任何你喜欢的名称;程序员通常使用 eevent

运行草图,将鼠标指针放置在显示窗口的某个位置,并使用滚轮。控制台应该会显示类似以下内容:

<MouseEvent WHEEL@407,370 count:1 button:0>

从此输出中,你可以确定鼠标事件类型是 WHEEL。事件发生时,水平鼠标位置为 407,垂直位置为 370(@407,370)。滚动方向为正(count:1)。当然,你的值会有所不同。

添加代码,使用 mouseWheel() 函数来调整画笔的大小。此代码还会在选色板下方显示一个画笔预览,反映当前画笔的颜色、大小和形状:

. . .def draw(): . . . # brush preview fill(brushcolor) if brushshape == ROUND: circle(palette/2, 123, brushsize) 1 paintmode = 'free'. . .def mouseWheel(e): # resize the brush global brushsize, paintmode 2 paintmode = 'select' 3 brushsize += e.count 4 if brushsize < 3: brushsize = 3 5 if brushsize > 45: brushsize = 45 redraw()

你不希望在调整画笔大小时进行绘画,因此 paintmode 被切换为 select 2。e.count 用来从事件变量中获取负/正滚动值,并将其添加到 brushsize 3。然而,必须包括检查(if 语句)以确保新画笔大小保持在合理范围内(介于 3 4 和 45 5 之间)。最后,redraw() 函数只运行一次 draw() 函数,以更新画笔预览并将 paintmode 切换回 free 1。

运行草图以确认你可以使用滚轮调整画笔大小,这会更新调色板中的画笔预览图(图 11-8)。

f11008

图 11-8:使用不同大小的画笔进行绘画

但是,有一个问题。当使用大画笔选择颜色样本时,颜料块可能会扩展到深蓝色的画布区域(图 11-9)。

f11009

图 11-9:使用大画笔选择颜色样本

为了解决这个问题,向 draw() 函数中添加一个 if 语句,以在鼠标悬停在调色板上时禁用绘画功能。使用 paintmode 变量来控制这一点:

. . .def draw(): print(frameCount) global painting, paintmode if mouseX < palette: paintmode = 'select' . . .

运行草图以确认你可以使用大画笔选择颜色样本,而不会有颜料块侵占画布区域。

现在你了解了鼠标事件的工作原理,如果需要 mouseDragged()mouseMoved() 函数,可以查阅在线文档。在第 252 页的“挑战 #11:为画图应用程序添加功能”中,你将尝试使用 mouseClicked() 函数。如果你想将鼠标指针从箭头更改为其他形状,可以使用 cursor() 函数。例如,你可以在 setup() 块中添加 cursor(CROSS) 来显示十字准星。

键盘交互

计算机的键盘设计继承自打字机。在这个过程中,计算机键盘增加了许多新的键位,如方向键、Esc 键和功能键,以及数字键盘以便更高效地输入数字。它们还配有修饰键(如 alt 和 ctrl),你可以与其他键组合使用,执行特定的操作。例如,Z、X、C 和 V 键与 ctrl 或  键结合使用,可以执行撤销/剪切/复制/粘贴操作。

在 Processing 中,键盘交互与鼠标交互类似,通过系统变量如 keykeyPressed,以及事件函数如 keyPressed()keyReleased()keyTyped() 来实现。

现在,让我们为画图应用程序添加选择颜色的键盘快捷键。

为画图应用程序添加键盘快捷键

要编写快捷键,你需要将 key 系统变量和 keyPressed() 事件函数结合使用。每次按下一个键时,keyPressed() 函数会执行一次。但是,按住一个键可能会导致重复调用该函数。操作系统控制这种重复行为,且不同用户的配置可能有所不同。Processing 会将最近使用的键值存储在 key 系统变量中。

在代码的末尾添加一个keyPressed()事件函数。暂时,这将打印控制台中的key值:

. . .def keyPressed(): print(key)

运行草图并按下不同的键。数字、字母和符号会如你所预期地显示在控制台中——当你按下数字 1 键时,显示1,按下 Q 键时,显示q,依此类推。如果开启了大写锁定,显示的将是大写字母。

要选择不同的颜色样本,将print()函数替换为使用数字键 1 到 6 的代码:

. . .def keyPressed(): global brushcolor, paintmode paintmode = 'select'  # color swatch shortcuts if str(key).isdigit(): k = int(key) - 1 if k < len(swatches): brushcolor = swatches[k] redraw()

Python 的isdigit()方法如果字符串中的所有字符都是数字,则返回True。这只适用于字符/字符串,并且能够很好地处理大部分key值,对于任何字母和符号返回False。然而,Processing 使用数字代码(换句话说,整数,而非字符串)表示特殊键(箭头键和修饰键)。因此,你需要使用str(key)将任何数字代码转换为字符串,以防止某些按键导致应用崩溃。如果key值是数字,Python 会减去 1 并将其赋值给变量k。因为swatches列表是从 0 开始索引的,颜色 1 等于swatches[0],依此类推。最终的if语句验证索引值(k)是否小于样本列表的长度——换句话说,是一个介于 0 到 5 之间的数字。redraw()函数会更新画笔预览。

画图应用程序可以用不同的颜色进行绘画,且笔触的厚度各不相同。尝试向你的画图应用添加其他功能。

挑战#11:添加画图应用功能

你可以添加的最有用的功能之一是一个清除按钮,这样当你想要一个空白的新画布时,就不需要关闭并重新打开应用程序了。你将编程一个按钮,重置画布为深蓝色。

向调色板添加一个标记为CLEAR的按钮:

. . .def draw(): . . . # clear button fill('#FFFFFF') text('CLEAR', 10, height-12)

这将在显示窗口的左下角绘制CLEAR,使用 Ernest 字体(见图 11-10)。

f11010

图 11-10:清除按钮

你可以使用mouseClicked()函数在鼠标按键点击的瞬间执行代码,就在松开按钮的时刻。像其他鼠标事件一样,这段代码只会执行一次,直到你重复该操作。将一个mouseClicked()函数添加到你的代码中:

. . .def mouseClicked(): circle(width/2, height/2, width)

如果你在显示窗口的任何位置点击,这段代码将在整个画图应用程序上绘制一个圆圈。现在,将circle()这一行替换为只响应清除按钮上的点击,而不响应该区域外的点击的代码。此外,这段代码还必须在画布区域上绘制一个深蓝色的正方形。

一旦你正确实现了清除按钮,尝试添加一个保存(为图片)按钮、橡皮擦、更多颜色样本,甚至可以加入一个颜色混合器。如果你需要帮助,可以在github.com/tabreturn/processing.py-book/tree/master/chapter-11-mouse_and_keyboard_interaction/paint_app/找到解决方案。

总结

在本章中,你学习了如何通过鼠标和键盘输入为你的草图添加交互性。你了解了 Processing 的系统变量,用于处理这些输入设备,以及当特定事件发生时触发的事件函数,这些函数只会执行一次。

Processing 支持多种输入设备,如麦克风、摄像头和游戏控制器,我鼓励你去探索这些功能。如果你想制作自定义输入设备,还可以将 Arduino 板连接到 Processing。

在本章中,你编写了一个简单的工具面板来选择颜色样本。许多软件和网页开发项目需要图形界面开发,许多图形用户界面(GUI)工具包提供了预制的控件集,例如按钮、复选框、滑块、下拉列表和文本框。如果你想构建更复杂的界面,Processing 也有图形界面库可以探索。对于 Python(Processing 之外),Tkinter、PyQt 和 Kivy 是一些选择。

在后记中,我将为你指向其他有用的资源,并建议你在创意编程冒险中可能考虑的下一步。

第十二章:后记

干得好!你已经读完了这本书。从那个第一次使用的print()函数开始,你已经走了很长一段路!你使用 Processing 的 Python 模式深入研究了随机性、周期性运动、欧几里得向量、交互性等等。你学会了如何编写代码来生成图案、动画和数据可视化。你还学会了如何从 CSV 和 JSON 文件中读取数据,并通过使用面向对象编程和模块来组织你的程序。如果你需要参考书中的示例,可以访问github.com/tabreturn/processing.py-book/或查看www.nostarch.com/Learn-Python-Visually/。该仓库包含每章挑战的解决方案。但仍有很多内容值得探索。

你新获得的 Processing、Python 和创意编程技能将为你进入一个日益扩展的创意技术领域提供通道,例如游戏、网络、增强现实/虚拟现实,甚至电影的视觉特效。我将在这里建议一些你可以继续探索的主题——即更多的高级 Processing 技巧、各种 Python 框架和不同的创意编程环境。你下一个大型编程项目可以是极具表现力的(像艺术画廊中的作品),也可以是高度实用的,或者介于两者之间的任何东西。

你可以从这里出发,朝许多方向发展。首先,看看其他项目以获得灵感——研究它们的构建方式、所使用的技术和编程语言等等。浏览 Creative Applications Network 网站上展示的作品合集(creativeapplications.net/)以及网络上的其他地方。

更多 Python 与 Processing

Processing 拥有一个庞大的社区,包括艺术家、极客、修补匠、设计师、研究人员、爱好者和教育工作者,他们通常聚集在 Processing 官方论坛(discourse.processing.org/)上。你可以与社区成员聊天,遇到问题时寻求帮助,并保持对新动态和事件的关注。该网站包括一个专门的 Processing.py 类别,以及一个用于分享你创作的画廊部分。

Processing 的官方 Python 模式参考资料可以在py.processing.org/reference/找到。每个条目包括描述和简短的代码示例。你还可以在网站的教程部分找到有用的教程;以下是你可能想在这里探索的主题:

  • “图像与像素”,让你能够在像素级别操作图形。你甚至可以通过这种方式创建你自己的类似 Photoshop 的滤镜。

  • Processing 的 3D 渲染模式“P3D”,用于通过 x、y 和 z 坐标绘制三维图形,并带有纹理和光照效果。

  • Processing 提供了众多库,涵盖了物理学、GUI、视频、AI、音频等方面。你可以在 Processing 的主页面上找到“Libraries”部分的链接(processing.org/)。这些库是为 Processing 的 Java 模式构建的,但大多数都可以在 Python 模式下使用。

关于最后一点,你会发现许多有用的算法是用其他语言编写的,尤其是 Processing 的 Java 模式。你可以在论坛上寻求帮助,将这些算法实现到 Python 模式中,但过一段时间后,你可能会自己将 Java 代码转换为 Python 代码。这也意味着你可以查看 Processing Java 资源,学习新的技巧来丰富你的 Python 模式草图。

The Nature of Code 是丹尼尔·希夫曼(Daniel Shiffman)为 Processing 的 Java 模式编写的极佳书籍,你可以在 natureofcode.com/book/ 在线阅读。它教你如何编写代码来模拟自然界中的现象。书中内容包括向量、力、粒子系统、物理学、自主代理、分形、进化算法和神经网络。你可以在 github.com/nature-of-code/noc-examples-python/ 找到相应的 Python 版本任务。

更多 Python

Python 是一种通用编程语言,适用于编程 AI、游戏、模拟、Web 应用程序,几乎涵盖了所有领域;每个领域都有多个 Python 库和框架可以探索。

No Starch Press 出版了几本很棒的 Python 书籍。关于游戏开发,阿尔·斯威加特(Al Sweigart)编写的 Invent Your Own Computer Games with Python 非常适合入门。对于一些极客式的有趣内容,马赫什·文基塔查兰(Mahesh Venkitachalam)编写的 Python Playground 和李·沃恩(Lee Vaughan)编写的 Impractical Python Projects 都是不错的选择。埃里克·马瑟斯(Eric Matthes)编写的 Python Crash Course, 2nd edition 是一本深入的 Python 书籍,涵盖了游戏(Pygame)、数据可视化(matplotlib)和 Web(Django)开发。

请记住,在写这本书的时候,Processing 的 Python 模式使用的是 Python 2.7,但我确保了书中的所有代码都是兼容 Python 3 的。你甚至可能在切换到 Python 3 开发环境时没有察觉到什么区别。

其他创意编码环境

如果你想学习一种不同的编程语言进行创意编码,可以考虑 Processing 的 Java 模式,以及 Processing 的 JavaScript(p5.js)和 Ruby(JRubyArt)变体。除此之外,还有 C++ 语言的 openFrameworks 和 Kotlin 的 OPENRNDR 用于创意编码。这并不是全部,但这个列表应该足以让你开始在正确的方向上寻找更多的资源。

如果你想编写可以与现实世界互动的设备程序,Arduino 提供了一个开源平台用于电子项目。它是一个相对便宜、可编程的开发板,你可以将其连接到传感器、电机、灯光和其他电子元件。你还可以让你的 Arduino 板与 Processing 草图进行通信。

posted @ 2025-11-27 09:16  绝不原创的飞龙  阅读(16)  评论(0)    收藏  举报