一、简介
在我们生活的这个时代,编码是无价的。它有能力提升你的职业生涯,你的未来前景,甚至你的智力。计算正在推动历史上最大的资本扩张之一,而现在是学习编码的最佳时机。
为什么要学编码?
我第一次认真接触编程是在大学。我们必须学习一门叫做 C 调的编程语言课程。我第一次不得不上这门课时没及格,第二次不得不重新上时勉强及格。考虑到那次失败,我远离编码的时间最长。我认为这是我根本不具备的天赋。后来,我继续将我的职业从工程转向视觉效果,因为我想在一个有更多创造性表达空间的领域工作。但是在视觉特效工作中,我开始意识到整个操作实际上是通过计算的力量来实现的。从使用的软件到促进生产的管道管理……编码无处不在。它允许电影公司为票房收入数亿美元的电影提供令人兴奋的效果。
在意识到编码在我的领域中的力量后,我决定踏上一段旅程去了解更多。最后自学了 Python,一种在视觉特效中广泛使用的编程语言。这样做是非常令人满意的。它不仅让我在视觉效果方面的工作更有成就,并创造出获奖的效果,还让我能够过渡到一个更有回报的软件开发职业。
编码与编程
您一定在相似的环境中听过编码和编程这两个术语,并且可能想知道它们之间有什么区别。在过去的几年中,编码已经成为使编程更容易为大众所接受的术语。基本上,前提是你可以在不实际编程的情况下编码并为数字经济做贡献。
让我给你举个例子:你可以使用 HTML 和 CSS 这样的网络语言,它们不是编程语言。因此,当用这些语言编码时,你并不是真的在编程,而是在设计或构建网站(下一节将详细介绍它们的用法)。但是你也可以用 JavaScript 编码,这是一种真正的编程语言。编程语言允许你让计算机“做”事情。每次你在编程的时候,你也在编码。但是当你编码的时候,你可能不是在编程。编码是一个更通用的术语,用于描述所有向计算机传达意图的情况。
基本上你可以把编程看作是编码的一个子集。但是说实话,这两个术语现在几乎可以互换使用。这本书的主要目的是教你如何编程。我们将使用编程语言 JavaScript 为编程目的进行编码。
关于 HTML 和 CSS
看着我学习编程的道路,我发现向初学者教授编码的一些努力有点欠缺。该领域的主要问题之一是使用 HTML 和 CSS 作为入门语言。
这些语言的问题在于它们甚至不是编程语言!HTML 是一种标记语言,用于以 web 浏览器可以理解的方式定义文档的结构。例如,HTML 教你如何为浏览器编写文本,这样浏览器就能知道哪部分是文档标题,哪部分是段落,等等…
同样,CSS 也不是编程语言。它是一种样式语言,允许我们对 HTML 文档进行样式化,使它们看起来更美观,并且理想地使它们比以前更友好。此外,即使 CSS 可以用来创建令人难以置信的好看的结果,它通常是非常不直观的工作,甚至对程序员来说也很难推理。学习 CSS,你不仅仅是在学习编程,如果设计网站不是你唯一的目的,你很可能会从事一项对初学者来说并不有趣的活动。
推动使用这些语言来教授编码是可以理解的。毕竟,考虑到 web 应用程序的巨大优势和它们在某些情况下的巨大利润,人们发现自己想要为 Web 构建自己的项目。如果你要建立一个网站,你需要在一定程度上使用这些语言。但是以这些语言为起点可能会对什么是编码产生误解。当您构建程序或应用程序时,编码可能是一项非常有益且引人入胜的活动,因为可能性的领域要大得多。如前所述,我们需要使用编程语言来构建程序,所以显而易见的问题是:“是什么让一种语言成为编程语言?”
你可以在维基百科上找到一个半正式的定义。但对我来说,一门语言要被认为是编程语言,它需要有某些控制结构,允许我们表达一些基本的操作。即使这个定义对初学者来说也没有什么意义。意思是在编程语言中有允许计算机执行逻辑操作的结构。下面是这种结构的一些例子,我们将在后面看到更多:条件允许程序根据给定的条件输出不同的结果,变量存储值或循环允许程序在期望的时间内重复操作。如果现在这些都没有意义,请不要担心;这本书的目的是让我们了解所有这些基本的编程概念。
几乎所有的编程语言都有这种基本结构,使我们能够构建更加复杂的应用程序。想想英语,或者你可能知道的任何其他语言。有动词、名词和形容词。利用这些积木,人们可以说最简单的事情,或者继续写令人惊叹的小说。这些都是 HTML 和 CSS 中缺少的构件,这使得人们错过了使用编程语言可以实现的目标。
在本书中,我们将学习所有这些基本结构,这些结构允许我们使用编程语言 JavaScript 向计算机传达我们的意图。
为什么要学 JavaScript?
有许多编程语言。这本书将教你如何通过使用非常流行的编程语言 JavaScript 来编码。
JavaScript 是最广泛使用的编程语言之一,因为它内置于每个 web 浏览器中。因此,几乎所有的网页和应用程序都在某种程度上使用 JavaScript。近年来,JavaScript 不仅开始用于网页中的用户交互编程,还用于服务器端后端应用、物联网(IOT)设备或 Android 或 iPhone 等平台的移动应用。尽管 JavaScript 知识起源于 web 开发,但它现在也适用于大量其他领域。
鉴于 JavaScript 的流行和无处不在,如果您遇到困难,找到关于它的资源和信息真的很容易。它背后有一个庞大而充满活力的社区。在流行的问答网站 StackOverflow 中,有超过一百万个问题与 JavaScript 相关。如果你最终用这种语言编码并陷入了一个问题,很可能其他人也有同样的问题,在这个网站上发布了一个问题,并得到了一个你可以借鉴的答案。
我不会详细说明是什么使编程语言成为动态或静态的,但是作为一种动态编程语言,JavaScript 代码比静态语言更简洁,也更容易编写。清单 1-1 和 1-2 是一些例子,在屏幕上显示单词“hello world”的简单语句是用不同的语言编写的。请注意,使用 JavaScript 编写相同的代码要短得多。
// Hello World in C++ (pre-ISO)
#include <iostream.h>
main()
{
cout << "Hello World!" << endl;
return 0;
}
Listing 1-1Displaying Hello World to the screen in C++ (Source: http://helloworldcollection.de/)
// Hello World in Java
class HelloWorld {
static public void main( String args[] ) {
System.out.println( "Hello World!" );
}
}
Listing 1-2Displaying Hello World to the screen in Java (Source: http://helloworldcollection.de/)
用 JavaScript 在屏幕上显示 Hello World:
console.log('Hello World');
学习 JavaScript 的另一个好处是,因为它是网络语言,你将能够以一种非常简单的方式与其他人分享你的创作。我认为在学习一项新技能时,能够这样做并得到反馈是一个重要的考虑因素。
总而言之,学习编程有很多原因,JavaScript 是一个很好的选择,因为它:
- 更容易写;
- 是大众化的,无处不在的;
- 具有广阔的应用领域。
为什么我们有不同的语言?
你一定想知道为什么有不同的语言,如果它们都有相似的特性。
这是一个很好的问题。不同的语言之所以存在,是因为它们在设计时考虑了不同的原则。它们中的一些可能更难输入,但是它们给你更多的控制你的程序的稳定性和速度。其他的可能更简洁,但是执行起来可能更慢。有些语言更适合某些任务。JavaScript 非常适合全栈 web 开发,Matlab 非常适合数学计算,C++在游戏编程方面占主导地位,Julia 用于数据科学。不过,这并不意味着你不能在这些领域使用其他语言。Unity 游戏引擎为游戏开发提供 C#。数据科学可以首选 Python。GoLang 或许多其他语言可以用于后端 web 开发。有时归结为开发人员更喜欢使用什么以及他们已经知道什么。有时它归结于给定项目的限制。
我曾经在视觉特效行业工作,我们在该领域使用的软件包可以使用 Python 或 C++实现自动化。考虑到我们所使用的工具所支持的内容,这些对于该领域来说都是很好的语言选择。除了知道一种编程语言实际上使你更有可能学会另一种语言这一事实之外,在视觉效果方面知道 Java 在很大程度上是没有用的,因为它们彼此之间共享相似的原则。
选择哪种语言作为你的第一语言有时会是一个艰难的选择,因为有很多可行的选择。有时选择是由应用程序域决定的。如果你想用虚幻引擎开发一个游戏,也许你应该学习 C++。但是,如果这是你第一次接触编程语言,你可能会面临如此陡峭的学习曲线,这可能会令人沮丧。
JavaScript 是作为你的第一编程语言学习的一个很好的选择。如前所述,它被广泛使用,并且具有广阔的应用领域,允许您尝试许多不同的应用程序。它背后有一个庞大而活跃的社区,并且有一个非常简洁的语法,使它更接近人类语言。
用 p5.js 学习 JavaScript
学习编程最具挑战性的方面之一是找到引人入胜的例子,这些例子不仅有趣、令人印象深刻,而且能说明手头的主题。一旦你掌握了窍门,编程是一项非常有益和吸引人的活动,但是对于一个初学者来说,一个专业程序员必须解决的大多数问题可能看起来无趣或者非常无聊。这就是为什么这本书在讲授这本编程入门书时使用了一个 JavaScript 库,一个名为 p5.js 的插件。p5.js 将允许您创建引人入胜的交互式和可视化作品,您在创建时会从中获得乐趣,它还将让您为软件开发打下坚实的基础。这个库的可视化本质将允许我们以图形的形式真实地看到脚本的结果,并发展对编程结构的深入理解。
p5.js 是一个编程库。编程库可以被认为是为特定目的而构建的代码集合,因此每当您需要执行与该目的相关的操作时,您都可以使用库,而不是自己构建该功能。库构建并扩展了语言的核心功能。对于 JavaScript,有超过 10 万个库允许您执行各种各样的操作。因此,在尝试实现您自己的功能之前,检查是否有人已经为您的需求创建了一个开源甚至是付费的库总是一个好主意。这个想法是,对于一个特定的问题,一个库将是一个久经考验的解决方案,你可以放心地利用它,而不是设计你自己的解决方案,这可能会给你正在开发的程序带来不可预见的问题。对于 JavaScript 来说尤其如此,因为核心语言没有任何内置的标准库;因此开发工作严重依赖外部库来解决常见问题。下面是几个有趣的库的例子,让您了解一下现有的库:
- Faker.js (
https://github.com/Marak/Faker.js
):产生海量虚假数据。 - 法郎(
https://github.com/wooorm/franc
):检测给定文本的语言。 - jimp (
https://github.com/oliver-moran/jimp
):图像处理库。 - cylon.js (
https://cylonjs.com/
):机器人技术、物理计算和物联网的机器人框架。
p5.js 是一个创造性的编码库,基于草图的思想。就像素描可以被认为是快速原型化想法的最简单的绘图方法一样,p5.js 的概念是编写最少的代码来将您的视觉、交互或动画想法转化到屏幕上。p5.js 是流行的库 Processing 的 JavaScript 实现,它基于 Java 编程语言。
值得一提的是,Java 和 JavaScript 是完全不相关的语言。JavaScript 以 Java 命名的原因是过去做出的一个不幸的品牌和营销决策。
p5.js 的简洁本质使它成为一个非常容易学习的库。但是不要让这种简单性欺骗你,让你相信 p5.js 的能力有限。p5.js 背后有大量令人印象深刻的功能、历史和社区,如果您想使用代码创建艺术、设计、动画或交互式作品,这将是一项有价值的学习投资。一个 p5.js 程序可以有几行到几千行代码。由于 p5.js 构建时考虑了简单性,所以有时小的 p5.js 程序被称为草图。尽管这是一个很聪明的描述方式,但我个人并不喜欢这种措辞,因为它混淆了你所做的毕竟是编程的事实。
你可以找到 p5.js 的实际应用,比如创建数据可视化(图 1-1 )。
图 1-1
Data visualization with p5.js
或者可以用来创作抽象的生成艺术(图 1-2 )。
图 1-2
Abstract generative art with p5.js
您甚至可以创建动画或交互式视觉效果。我们将在本书结束时使用 p5.js 构建一个互动游戏!
二、入门指南
正在安装 p5.js
有几种方法可以开始使用 p5.js 和 JavaScript。一种选择是访问 p5.js 网站( https://p5js.org/download
)并将 p5.js 源代码下载到您的系统上(见图 2-1 )。
在编写本演练时,下载页有一个名为“p5.js complete”的链接,其中包括 p5.js 库和一个示例项目。下载这个归档文件,并在其中找到名为empty-example
的文件夹。在这个文件夹中,您将找到两个文件:一个是可以编写 JavaScript 代码的sketch.js
文件,另一个是可以用 Chrome 等网络浏览器启动的index.html
文件,它将执行并显示sketch.js
文件中 JavaScript 代码的结果。你也可以在我的 GitHub 库上找到这些文件的副本: https://github.com/hibernationTheory/p5js-complete
。
即使您可以使用记事本这样的纯文本编辑器来更改sketch.js
JavaScript 文件的内容,您也可能需要使用代码编辑器,例如“Sublime Text”。
代码编辑器与文本编辑器非常相似,就像记事本或 Word,但是它有一些特殊的功能,可以使编码变得更加容易,比如突出显示给定编程语言的特殊单词,在这种情况下,该语言是 JavaScript。Sublime Text 是一个你可以使用的代码编辑器,可以免费下载和评估。
也许开始使用 p5.js 最简单的方法是使用在线编辑器。在线代码编辑器可以在网络浏览器中使用,不需要你在系统上安装任何东西。当我学习的时候,这是我喜欢的工作方式,因为它让我很容易开始。
在编写本书时,可以通过以下链接找到一个易于使用的在线代码编辑器:
p5.js 在线编辑- alpha )
如果由于任何原因无法访问以上链接,您也可以尝试我的 Codepen 帐户上的 p5.js 模板:
【Codepen - p5.js 简单模板】( https://codepen.io/enginarslan/pen/qpBBXz?editors=0011
)。CodePen ( https://codepen.io
)是一个社交开发平台,允许你在浏览器中编写代码,并与其他开发者分享你的作品。这是一个很好的开发和实验环境。Codepen 和上面提到的 p5.js 编辑器的区别在于,p5.js 编辑器只允许你在它自身内部运行 p5.js 相关代码,而 Codepen 可以执行任何前端代码。
图 2-2
p5.js online editor
图 2-1
Web page to download p5.js source code
在线编辑器的工作原理是,每当我们准备好要执行的代码时,我们就按页面顶部的播放按钮。这个播放按钮将在右侧面板上显示我们代码的结果。Codepen 的在线编辑器略有不同,它会在您更改代码时自动执行代码。此时按下Play
按钮不会有什么作用,因为我们没有编写任何在屏幕上绘制形状的代码。我们将会看到生成一个空屏幕。但是正如我们所看到的,这个编辑器已经编写了一些代码。我们看到的这段代码对于我们将要编写的几乎所有 p5.js 程序都是必需的,所以为了方便起见,我们把它包含在这里(清单 2-1 )。
function setup() {
createCanvas(400, 400);
}
function draw() {
background(220);
}
Listing 2-1
Default p5.js code
让我们暂时删除这段代码。在我们开始使用 p5.js 学习 JavaScript 之前,我们将了解一些 JavaScript 的基础知识。
你可以在 GitHub 资源库找到我们将在本书中使用的代码示例: https://github.com/hibernationTheory/coding-for-visual-learners
。
JavaScript 简介
我们可以在屏幕上写一些简单的东西。这是将这两个数字相加的有效 JavaScript 代码。如果我们通过按下Play
按钮来执行这段代码,我们仍然看不到任何东西。这有点令人失望,因为我们至少期望看到这个计算的结果。
为了能够在屏幕上看到 JavaScript 操作的结果,我们可以使用一个名为console.log()
的函数。
函数是一种编程结构,其中包含为执行特定操作而编写的其他代码。函数允许我们通过使用定义好的函数名调用它们来执行复杂的操作。当我们调用一个函数时——我们也可以称之为执行函数——我们会写下它的名字,在这里是console.log
,并在它旁边放上括号。如果函数需要一个输入来执行它的功能,那么我们将在括号中提供这个输入,就像我们在这个例子中所做的那样。
console.log
是一个内置的 JavaScript 函数,它在编辑器下面的控制台中显示——或记录——给定值。当我说内置时,这意味着大多数 JavaScript 执行环境都有这个功能。例如,web 浏览器的界面中有一个名为console
的部分,我们可以通过开发者工具访问它。p5.js 和 Codepen 在线编辑器的编辑区下面也有一个叫做console
的部分。
我们还可以拥有用户自定义的功能,这些功能是我们自己创建的,除非我们以某种方式与他人共享,否则其他任何人都无法使用。像 p5.js 这样的库有一堆自己的函数。我们将使用 p5.js 函数在屏幕上绘制形状,并创建各种交互式和动画视觉效果。稍后我们将深入探讨函数的概念,但现在,我们知道 JavaScript 中有一个名为console.log
的函数,它接受一个值并在编辑器下的控制台中显示该值。最初,我们将学习的其他函数的名称中没有点。console.log
在这个意义上有点不同,但是使用点的原因将在后面解释。
让我们在代码中再添加几个console.log
语句(清单 2-2 )。
console.log(1 + 1)
console.log(5 + 10)
console.log(213 * 63)
console.log(321314543265 + 342516463155)
Listing 2-2
console.log statements
清单 2-3 显示了一旦清单 2-2 中的代码被执行,控制台内将显示的结果。
2
15
13419
663831006420
Listing 2-3Results for console.log statements
一个要点应该是代码自顶向下执行。有一些编程结构改变了这个流程,但是我们将在后面看到它们。另一个要点应该是,计算机不介意处理大量数据。我们可以对它们进行人类需要几天才能完成的艰难操作。
在清单 2-2 的最后一个console.log
语句中,我们有两个大得离谱的数字。如果我们想在下一行中使用该操作的结果数字并从中减去 10 呢?现在要做到这一点,我们必须再次键入该数字:
console.log(321314543265 + 342516463155 - 10)
这显然非常浪费。但幸运的是,计算机擅长的另一件事是存储和记忆数值。因此,我们可以创建一个叫做variable
的东西来保存这个值。在编程语言中,变量是指一个值的名称。因此,我们可以使用变量名来引用该值,而不是再次键入该值。这是如何工作的:
var bigNumber = 321314543265 + 342516463155
console.log(bigNumber)
console.log(bigNumber - 10)
我们使用关键字var
创建一个名为bigNumber
的变量。var
是我们在创建变量时需要用到的关键词。在关键字var
之后,我们给这个变量一个名字,在这个例子中是bigNumber
。
选择对当前上下文有意义的变量名很重要。在这个例子中,这可能没有太大关系,但是随着我们的程序变得越来越复杂,有意义的变量名可以帮助我们理解在读取代码时发生了什么。因此,将这种包含大量数字的变量命名为cat
没有太大的意义,可能会让其他可能阅读我们代码的人感到困惑。如果我们几个月后再回到我们的代码上,这甚至会让我们感到困惑。程序员总是努力使他们的代码尽可能的可读。
一旦声明了这个变量,我们就可以使用等号运算符给它赋值。乍一看,这似乎不太寻常。在数学中,等号运算符用于表示两个值相等。这里我们用它给一个变量赋值。它获取运算右侧的值,并将其赋给左侧的变量。这是许多编程语言中都存在的一个非常常见的过程。
现在我们有了一个指向值的变量,我们可以在操作中使用这个变量名而不是值本身。如前所述,变量名有意义是件好事。还有一些规则决定了我们可以用什么和不可以用什么作为变量名。例如,我们不能使用破折号或感叹号等特殊字符,也不能在变量名中使用空格。另一个限制是我们不能使用某些 JavaScript 保留名作为变量名;我们不能调用我们的变量var
,因为这个名称已经被 JavaScript 使用。如果我们试图使用var
作为变量名;在var var = 5
中,JavaScript 会抛出一个错误。
这里提到的规则可能会让你感到不安。毕竟,编程应该是有趣的,对吗?但是不用担心;预留名单比较短,不需要背。随着你对这门语言了解的越来越多,你也会更好地意识到应该避免哪些名字。
关于规则,还有一个规则应该提到。JavaScript 需要我们在每个语句后放置分号。如果我们不这样做,我们的程序仍然可以工作,但可能会在某些以后很难识别的边缘条件下失败。因此,在每个语句后使用分号是一个好主意,尽管这意味着我们要做更多的工作。前面的代码实际上应该如清单 2-4 所示编写:
console.log(1 + 1);
console.log(5 + 10);
console.log(213 * 63);
var bigNumber = 321314543265 + 342516463155;
console.log(bigNumber);
console.log(bigNumber - 10);
Listing 2-4Using semicolons
注意,做bigNumber - 10
不会改变bigNumber
变量的初始值。在下面的例子中,console.log
语句仍然会输出 10。
var x = 10;
x + 5;
console.log(x);
如果我们想改变一个变量的值,那么我们需要给它赋一个新值(清单 2-5 )。
var bigNumber = 321314543265 + 342516463155;
console.log(bigNumber);
bigNumber = 3;
console.log(bigNumber);
Listing 2-5Overriding the variable value
在这个例子中,console.log
将显示值3
,因为我们在第 3 行用另一个值覆盖了初始值。
JavaScript(以及其他语言)中有数据类型的概念来区分不同种类的值。我们一直在使用的这些数字属于一种叫做Number
的数据类型。还有另一种称为String
的数据类型,用于表示文本信息。
在 JavaScript 中,我们不能只写一个单词就期望它表示数据。比如,我们想把console.log
这个词hello
。如果我们现在这样做,我们会注意到我们得到了一个错误。JavaScript 不理解hello
是什么意思。它假设它是一个尚未定义的变量。
console.log(hello);
> 1: Uncaught ReferenceError: hello is not defined
但是如果我们真的想把单词hello
输入到电脑中呢?有一些处理文本数据的程序,需要处理给定的名称或地址等。在这种情况下,我们可以使用引号来提供数据,这意味着我们提供的值是一个string
。
console.log('hello');
JavaScript 这次没有抱怨。每当我们处理文本数据时,我们需要把它放在引号中;这将使其注册为string
。当我说文本数据时,它也可以是数字。一个string
可以由数值组成:
console.log('1234');
在这种情况下,它们不会被视为我们可以用来执行数学运算的数学数字,而只是作为文本。
我们可以对字符串执行操作,但是它不会产生与我们使用数字执行这些操作时相同的结果。我们实际上可以把两个字符串加在一起:
console.log('hello' + 'world');
> 'helloworld'
这将会把这两个词结合在一起。当我说我们不能对包含数值的字符串执行数学运算时,意思是这样的:
console.log('1' + '1');
> '11'
在这种情况下,数值不被视为数字,而是被视为字符串,它们不是被加在一起,而是被组合在一起。这种组合字符串的行为在编程中通常被称为concatenation
操作。
String
听起来可能是一个奇怪的名字选择,但它指的是字符串。所以就计算机而言,string
实际上是单个字符的集合。我们可以用单引号'
或者双引号"
来定义strings
,但是我们必须用我们选择用来开始定义的相同符号来结束字符串。同样,在我们的程序中,我们不应该对一个字符串使用一种类型的引号,而对另一个字符串使用另一种类型的引号。开发程序时,一致性非常重要。
在结束本节之前,另一件值得一提的事情是comments
的概念。注释允许我们将计算机无法执行的东西写入程序,如清单 2-6 所示。
// various examples. (this is a comment)
console.log(1 + 1);
console.log(5 + 10);
console.log(213 * 63);
var bigNumber = 321314543265 + 342516463155;
console.log(bigNumber);
console.log(bigNumber - 10);
Listing 2-6Example for using comments in our program
JavaScript 会忽略以双斜线//
开头的行。双斜线允许我们对单行进行注释;如果我们需要对多行进行注释,我们要么需要在每行的开头使用双斜线,要么使用/* */
符号,如清单 2-7 所示。
// various examples
// disabling the first 3 lines by using multiline comments:
/*
console.log(1 + 1);
console.log(5 + 10);
console.log(213 * 63);
*/
var bigNumber = 321314543265 + 342516463155;
console.log(bigNumber);
console.log(bigNumber - 10);
Listing 2-7Using // and /* */ for comments
信不信由你,这已经足够让我们开始使用 p5.js 了。如果你正在使用代码编辑器,点击New Project
按钮可以得到一个新的编辑器窗口,其中有我们将用于 p5.js 代码的模板。
p5.js 入门
当我们在 p5.js 代码编辑器中启动一个新项目时,我们看到的是两个函数声明,它们的名字分别是:setup
和draw
(列表 2-8 )。
function setup() {
}
function draw() {
}
Listing 2-8
Default function declarations
我们编写的几乎每个 p5.js 程序都需要进行这两个函数声明。p5.js 在我们的代码中找到这些函数定义,并执行写在其中的任何内容。但是这些功能的执行方式有所不同。
函数setup
内部的块,即花括号之间的区域,是我们编写代码的地方,这些代码将被执行来初始化我们的程序。写在setup
函数中的代码在draw
函数之前只执行一次。
function setup() {
// write your code for setup function inside these curly brackets
}
draw
函数是真正神奇的地方。任何写在draw
函数中的代码都会被 p5.js 重复执行。这允许我们创建各种各样的动画和交互式作品。
p5.js 确保在执行draw
函数之前执行setup
函数。再次重申,p5.js 只执行了一次setup
函数,但却一遍又一遍地执行draw
函数(实际上接近每秒 60 次)。这就是我们如何使用 p5.js 创建互动的动画内容
通过将console.log
语句放在代码中的不同位置,我们实际上可以看到这一点。在setup
函数内部、draw
函数内部以及这两个函数外部使用不同的值放置一个console.log()
语句(清单 2-9 )。
function setup() {
console.log('setup');
}
function draw() {
console.log('draw');
}
console.log('hello');
Listing 2-9Logging the behavior of setup and draw functions.
让我们执行这段代码,并立即尝试停止它。我们会注意到消息hello
被显示为第一件事。这是预期的行为。我们的函数调用应该由 JavaScript 执行。令人意想不到的是,setup
和draw
函数也被执行了。这是意外的,因为这些只是函数声明;它们定义了函数的行为,但是我们仍然需要执行这些函数才能使用它们。
这意味着,如果我们只是使用 JavaScript,我们需要显式调用setup
和draw
函数,以便显示其中的console.log
消息:
setup();
draw();
console.log('hello');
但是我们不需要使用 p5.js 库来实现这一点。由于 p5.js 库是如何构建的,它寻找名为setup
和draw
的函数声明,并为我们执行这些函数。p5.js 之所以控制这些函数的执行,是因为它以一种非常特定的方式执行它们。
p5.js 只执行一次setup
函数,然后继续以重复的方式执行 draw 函数,这样如果我们不停止这个过程,它就会一直工作下去。对于任何图形界面来说,这都是一个非常标准的行为——想想网络浏览器、你玩的游戏或者你使用的操作系统。这些只是持续工作并显示在屏幕上的程序,直到我们明确地关闭它们。这就是为什么 p5.js 为draw
函数创建了一个执行循环,这样事情就会持续出现在屏幕上,而不是出现一秒钟然后消失。
关于函数的更多信息
让我们多谈谈函数,因为它们是我们将要编写的程序的组成部分。
函数名通常是动词。它们表示通过执行该功能可以执行的特定操作。假设说,我们可能有一个名为drawCat
的函数,当它被调用时,可以在屏幕上画一只猫:
drawCat();
然而,这根本不是假设,因为我实际上为这一章创建了一个名为drawCat
的猫绘图函数(图 2-3 )。我们可以自由地用 JavaScript 创建任何我们想创建的函数,这给了我们在编写应用程序时巨大的力量。
图 2-3
The graphic output of the drawCat
function
好吧,平心而论,这个函数在画一只猫方面做得并不太好。
要使用一个函数,我们通过它的名字来调用它,然后在它后面加上括号来执行这个函数。有时,根据函数的创建或定义方式,函数会被参数化。这意味着它们可以接受会影响函数结果的输入值。例如,drawCat
函数可能得到一个数字输入,它将决定所画的猫的大小。或者可能输入的数字决定了屏幕上会吸引多少只猫。这真的取决于这个函数是如何构造的。
在我们的示例中,我创建的这个函数可以获得一个输入,该输入允许我们更改在屏幕上绘制的猫头的大小(图 2-4 ):
图 2-4
Drawing a cat face
drawCat(2);
不幸的是,p5.js 没有自带drawCat
函数——我不得不自己创建——但是它有很多其他有用的函数,可以让我们以简单的方式执行复杂的任务。为了能够使用 p5.js 库做任何事情,我们将使用它附带的函数,这些函数是由创建这个库的聪明人编写的。
这里有一个来自 p5.js 库中的函数,可能我们将要写的所有草图都需要这个函数:函数createCanvas
。createCanvas
函数的作用是在网页内部创建一个绘图区域画布,供我们工作。但是为了让这个函数工作,我们需要为它提供两个逗号分隔的值:绘图区域的宽度和高度。我们应该在setup
函数中调用createCanvas
函数,因为它只需要执行一次,而且需要在我们进行任何绘图之前执行。
让我们为这个函数提供值800
和300
,并执行我们的草图来看看发生了什么(清单 2-10 )。看起来变化不大,但启动的浏览器窗口的大小似乎增加了。它现在使用我们提供的维度。让我们再次更改尺寸,以查看窗口大小的更新。
function setup() {
createCanvas(800, 300);
}
function draw() {
}
Listing 2-10Working with the createCanvas function
还有一个我们会经常用到的函数,叫做background
。background
函数使用给定的值设置画布的颜色。我们将在另一章中研究颜色值是如何在 p5.js 中表示的,但是现在,我们可以只给这个函数提供值(220,220,220)
来看到背景变成浅灰色(清单 2-11 )。
function setup() {
createCanvas(800, 300);
background(220,220,220);
}
function draw() {
}
Listing 2-11Working with the background function
正如我们再次看到的,代码是从上到下执行的。p5.js 首先为我们创建画布,然后将背景设置为灰色。
值得再次强调的是:setup
和draw
是 p5.js 正确工作所需的函数定义。当我们使用 p5.js 时,我们的工作是确定在这些由 p5.js 执行的函数中放置了什么。这是由 p5.js 的架构决定的。p5.js 的创建者希望确保我们将编写的一些代码只执行一次,用于初始化和设置目的,而一些代码将一直执行,用于绘图、动画和交互目的。
我们在这些函数定义中使用了 p5.js 库中的函数,比如createCanvas
和background
。这些函数已经被其他人定义了,所以我们实际上不知道它们里面包含了什么代码。但是我们并不真的需要这些知识,因为我们关心的只是它们做什么以及如何使用它们。
函数允许我们以简单的方式执行复杂的任务。通过使用createCanvas
函数,我们不需要知道在页面中创建 canvas 元素需要做哪些工作。这些细节对我们来说是隐藏的、抽象的。我们只需要知道如何调用这个函数,让它为我们工作。
最后,我们将再调用一个函数,这次是在draw
函数定义中,在页面上绘制一个矩形(清单 2-12 )。
为了画一个矩形,我们将利用一个名为rect
的函数。rect
函数要求我们为它提供四个输入值:画布绘制区域内矩形左上角的 x 和 y 位置,以及矩形的宽度和高度值。
在不知道 p5.js 中坐标是如何工作的情况下,我们将只提供这个函数 x 值为 50,y 值为 100,宽度为 200,高度为 100(图 2-5 )。
图 2-5
Output of the rect function
function setup() {
createCanvas(800, 300);
background(220,220,220);
}
function draw() {
rect(50, 100, 200, 100);
}
Listing 2-12Drawing a rectangle
通过调用这个函数,我们在屏幕上画出了第一个形状!
p5.js 中的坐标
至此,我们花点时间解释一下 p5.js 中的坐标系是如何工作的。
为了确定平面上任意一点的位置,我们使用一个双轴坐标系。纵轴称为 Y 轴,横轴称为 X 轴。这两个轴相交的点称为origin
。在画布中,我们绘制形状的地方,原点在画布的左上角。从下面开始,Y 值增加;向右,X 值增加(图 2-6 )。
图 2-6
Coordinate origins
当我们在屏幕上绘制一个矩形时,提供的坐标定义了矩形的左上角(列表 2-13 和图 2-7 )。
图 2-7
Drawing a rectangle
function setup() {
createCanvas(800, 300);
background(220,220,220);
}
function draw() {
rect(400, 150, 100, 100);
}
Listing 2-13Drawing a rectangle
如果这不是您想要的行为,我们可以调用另一个名为rectMode
的 p5.js 函数,并为其提供值CENTER
来改变矩形在我们的程序中的绘制方式(清单 2-14 )。由于这个函数更像是一个设置和初始化相关的函数,我们将把它放在setup
函数定义下。
图 2-8
Output for a centered rectangle
function setup() {
createCanvas(800, 300);
background(220,220,220);
rectMode(CENTER);
}
function draw() {
rect(400, 150, 100, 100);
}
Listing 2-14Using the
rectMode function and CENTER value
p5.js 中还有一个ellipse
函数用来画圆形。ellipse
的工作方式与rect
功能非常相似。首先,两个参数是椭圆中心的 x 和 y 坐标,第三个参数是水平半径,第四个参数是垂直半径。所以为了能够用ellipse
函数画一个圆,我们需要为它提供相等的水平和垂直半径值(清单 2-15 )。
如果您正在尝试将这些形状绘制到屏幕上,您可能已经注意到,无论何时调用 shape 函数,它都会将自己绘制在前面的形状之上。我们可以改变函数调用的顺序来影响形状的堆叠顺序。
图 2-9
Output for an ellipse and centered rectangle
function setup() {
createCanvas(800, 300);
background(220,220,220);
rectMode(CENTER);
}
function draw() {
rect(400, 150, 100, 100);
ellipse(350, 120, 100, 100);
}
Listing 2-15Using the ellipse function
我要介绍的另一个绘图函数是line
函数。顾名思义,line
函数在屏幕上画一条线。我们需要为line
函数提供四个参数:开始的 x 和 y 坐标以及结束的 x 和 y 坐标。稍微玩一下 line 函数;这将让您很好地了解 p5.js 中的坐标系是如何工作的。例如,您可以尝试绘制一个跨越整个画布的 X。
摘要
在这一章中,我们快速开始使用 p5.js,并在屏幕上画出形状。
我们已经看到,我们需要在两个名为setup
和draw
的函数定义块中编写代码。任何只需要执行一次的东西都被放在setup
函数下,任何我们想要制作动画或与之交互的东西都被放入draw
函数中。将我们的代码写入这两个函数是 p5.js 要求我们做的事情。它不是一般的编程原则、惯例或类似的东西。我们可以使用不同的库,它不需要对我们的代码进行这种结构化。这个需求与 p5.js 作为一个库的架构有关。我们需要从这两个函数定义开始我们所有的 p5.js 草图。
像这样需要重复编写的代码被称为boilerplate
代码。有很多样板文件从来都不是一件好事,因为我们会发现自己不得不重复我们的工作很多,但在这种情况下,样板文件的数量是非常容易管理的。
在这些函数定义中,我们使用了 p5.js 库中的函数,比如createCanvas
、background,
和一些形状函数,比如rect
。如前所述,函数是通用的编程结构,它允许我们将代码捆绑在一起以实现可重用性。函数也从我们身上抽象出大量的复杂性。我们不需要知道一个函数是如何工作的;我们只需要知道如何使用它。我们完全不知道createCanvas
实际上是如何在网页中创建画布元素的。只要我们知道如何使用这个功能就没关系。想到开车;我们不一定需要知道内燃机如何工作才能驱动它。我们只需要知道如何使用方向盘、踏板等与汽车互动。类似的想法也适用于函数。
稍后,我们将创建我们的函数来管理程序的复杂性,并创建可重用的代码。
实践
尝试重新创建图 2-10 中的图像。
图 2-10
Practice image
三、p5.js 中的颜色
现在我们可以在 p5.js 中绘制形状,让我们看看如何在草图中控制颜色。通过将值220, 220, 220
传递给background
函数,我们已经给背景分配了浅灰色。
p5.js 中的颜色函数
p5.js 默认使用 RGB 颜色系统,其中 R 代表红色,G 代表绿色,B 代表蓝色。这意味着我们通常需要将这三个颜色分量传递给一个颜色接受函数来设置所需的颜色。这些颜色分量中的每一个都可以具有 0 到 255 之间的值。这意味着如果我们要将0, 0, 0
传递给background
函数,我们将得到黑色背景,如果我们要传递255, 255, 255
,我们将得到白色背景。p5.js 是一个有用的库,当我们希望这三个值相等时,它允许我们传递一个值。这意味着不是通过0, 0, 0
;我们也可以只传递一个 0。
每当我们有等量的这三种颜色成分时,最终的颜色将是白色、黑色或灰色。因此,如果我们想要一个灰度颜色,向颜色设置函数传递一个值是很有用的。但是如果我们想要颜色中的色调,那么我们需要传递所有这三个值,以便能够指定我们想要的每个分量的数量。数字 255 是颜色分量可以接受的最大值;因此,如果我们将255, 0, 0
作为一种颜色传递给background
函数,我们将得到一种纯红色。如果我们通过0, 255, 0
,那么我们将得到纯绿色,等等。
RGB 颜色模型是一种加色模型,这意味着将这些颜色以最大强度相加将产生白色,而减色法是将所有颜色相加将产生深褐色。如果您不太熟悉使用加色 RGB 颜色,通过修改这些值来找到您想要的确切颜色可能会有点困难。如果是这种情况,你可以使用在线颜色挑选服务来帮助你找到想要的颜色。在线搜索“拾色器”一词会得到大量结果,您可以使用这些结果来识别所需颜色的 RGB 成分。这里有一个来自 Firefox 的示例服务(图 3-1 )。
图 3-1
Firefox color picker tool
使用这样的服务,您可以记下与您选择的颜色相对应的 RGB 值,并在 p5.js 中使用这些值。
我们实际上可以将第四个参数传递给一个颜色设置函数。第四个参数称为颜色的 alpha 分量,控制颜色的不透明度,同样接受 0 到 255 之间的值。0 表示完全透明,255 表示完全不透明。
因此,我们可以将单个值、三个值或四个值传递给颜色设置函数。我不想给你太多的信息,但是我们也只能传递两个参数。如果我们要这样做,我们将设置一个灰度颜色和一个灰度颜色的 alpha 组件。
如果这种丰富的选择看起来势不可挡,请记住它们是为了我们的方便。p5.js 可以将颜色函数限制为仅处理四个输入,这将覆盖所有情况,但当我们只需要像不透明白色这样的东西时,提供额外的数据将非常耗时,这种情况经常发生。看起来 p5.js 的开发人员足够聪明地构建了他们的函数,因此它们会基于不同数量的参数产生不同的输出。
更改形状颜色
知道 p5.js 中的颜色是如何工作的很好,但是我们目前只能改变背景的颜色。为了能够改变形状的颜色,我们将不得不使用更多的函数。
我们应该知道的第一个函数是fill
。fill
允许我们设置形状的填充颜色。填充颜色是填充形状内部的颜色,如果您想知道形状还有什么其他颜色控制,还有定义形状轮廓颜色的描边颜色。填充和描边的默认颜色分别是白色和黑色。除线条之外的所有形状都有填充和描边颜色。
如前所述,我们可以通过调用fill
函数并将颜色参数传递给该函数来设置形状的填充颜色。fill
功能会将当前颜色设置为选择的颜色,直到我们使用另一个fill
功能将颜色设置为其他颜色。
stroke
功能以类似的方式工作。我们给它传递颜色参数,它为所有形状设置笔画的颜色,直到下一个笔画函数。在前一个函数之后出现的fill
或stroke
函数会覆盖前一个函数的设置。
此时,需要知道的另一个有用的函数是strokeWeight
,它允许我们设置轮廓的粗细。
清单 3-1 是一个小草图,它利用了我们在本章中学到的一些函数。在图 3-2 中可以看到 3-1 的列表结果。
图 3-2
Output showing the use of fill, stroke, and strokeWeight functions
function setup() {
createCanvas(800, 400);
}
function draw() {
background(220);
// circle 01
fill(51, 51, 51);
strokeWeight(2);
stroke(75);
ellipse(400, 200, 300, 300);
// circle 02
stroke(0);
fill(255, 53, 139);
ellipse(400, 200, 275, 275);
// circle 03
fill(1, 176, 240);
ellipse(400, 200, 250, 250);
// circle 04
fill(174, 238, 0);
ellipse(400, 200, 150, 150);
}
Listing 3-1Using fill, stroke, and strokeWeight functions
注意我们是如何在我想要设置颜色的形状前使用fill
函数的。我们一直用它来切换不同椭圆的颜色。
另外两个值得一提的函数是noFill
和noStroke
函数。顾名思义,当被调用时,这些函数将分别去除形状的填充和描边。调用这些函数时没有任何参数。
noFill();
noStroke();
摘要
在这一章中,我们没有看到任何新的 JavaScript 功能或新的编程结构。我们刚刚看了 p5.js 库的一些操作原理和它附带的一些特定函数。特别是,我们了解了一些颜色设置函数在 p5.js 中是如何工作的,比如fill
、stroke
和strokeWeight
。我们还学习了与填充和描边操作相关的其他功能,如noStroke
和noFill
。我们学到的另一件事是 RGB 颜色模型。
尽管这一章并没有真正提升我们的 JavaScript 编程知识,但我认为有一点非常有价值。你可能会对自己说,你对创造性编码不感兴趣,在学过编码之后,不需要这本书的 p5.js 具体信息。但是,这些操作原则(如使用加法 RGB 值)或概念(如填充和描边)是如此常用,以至于即使我们所学的内容可能看起来非常特定于 p5.js,它们也是许多其他绘图库或程序使用的一般原则或概念。理解它们将在我们学习如何编程的旅程中很好地为我们服务。
实践
构建清单 3-1 中的脚本,其中一个变量将控制所有圆的大小(意味着改变该变量将改变所有圆的大小),另一个变量将控制所有圆的半径差(结果如图 3-3 和图 3-4 )。
图 3-4
Practice Image - 2
图 3-3
Practice image
四、运算符和变量
在第 1 和 2 章中,我们学习了可以在 JavaScript 中使用的变量和数学运算。在这一章中,我们将运用这些知识。
设置
让我们首先创建几个形状来处理一些东西。使用ellipse
和rect
函数,让我们创建一个大致类似于手推车的形状(列出 4-1 和图 4-1 )。
图 4-1
Output of Listing 4-1
function setup() {
createCanvas(800, 300);
}
function draw() {
background(220);
ellipse(100, 200, 50, 50); // left wheel
ellipse(200, 200, 50, 50); // right wheel
rect(50, 160, 200, 20) // cart
}
Listing 4-1Creating a cart using
rect and ellipse
functions
看着我们在图 4-1 中的草图,我对它的位置并不完全满意。我现在希望我们把它画得更靠右一些。现在移动形状意味着我们需要增加每个形状函数的 x 位置参数的值。
让我们假设我们想给所有这些指定 x 位置的数字加上 150。我们可以试着在脑子里算算,然后把结果输入进去,但幸运的是,我们可以用 JavaScript 轻松地进行数学运算。不用输入加法的结果,我们只需输入所需的运算,JavaScript 就会为我们完成计算(清单 4-2 和图 4-2 )。
图 4-2
Output of Listing 4-2
function setup() {
createCanvas(800, 300);
}
function draw() {
background(220);
ellipse(100 + 150, 200, 50, 50);
ellipse(200 + 150, 200, 50, 50);
rect(50 + 150, 160, 200, 20)
}
Listing 4-2Using Math Operations
同样的事情也适用于其他运营商;我们可以用类似的方式做减法、乘法或除法。
对于操作符,我们需要记住的一点是操作的顺序。你可能已经从你的数学课上了解到了这一点,但是有些运算符比其他运算符更重要。例如,如果我们想给一个数加 2,然后乘以 4,我们可能会写这样的代码:10 + 2 * 4
但是在这个运算中,乘法将发生在加法之前。在加到 10 之前,2 将与 4 相乘,因此上面的操作将产生 18,而不是预期的值 48。
为了能够控制运算的顺序,我们可以使用括号。比如,我们可以这样写上面的方程:(10 + 2) * 4
括号内的任何内容都将在其他操作之前进行计算。在运算顺序上,先有括号,再有乘除法,再有加减法。
变量
能够对这样的表达式求值,将使我们的计算工作变得更容易。但我认为,在这个例子中,真正的问题是需要在这三个不同的地方输入相同的数字。这非常重复、费力,并且容易出错。这是一个使用变量会很有用的例子。
每当我们需要一个值,并且需要在多个地方使用该值时,我们会希望将该值存储在一个变量中。使用变量的好处是,如果我们需要更新变量的值,我们只需要在一个地方完成。让我们更新这个例子来使用一个变量。
记住如何创建变量。我们将从使用关键字var
开始。使用这个关键字非常重要,原因将在后面讨论。
然后我们会为变量选择一个名字。选择一个有意义的名字也很重要。调用这个变量offset
或x
可能有意义,因为我们将使用它来偏移 x 轴上的形状。使用合理的名字将有助于他人甚至我们理解我们的代码。我们总是希望我们的程序尽可能具有可读性。
现在我们有了一个指向值的变量,我们可以在操作中使用这个变量,而不是值本身。这样做,我们只需要从一个点改变这个变量的值,就可以看到形状在移动(清单 4-3 )。
function setup() {
createCanvas(800, 300);
}
function draw() {
background(220);
var offset = 150;
ellipse(100 + offset, 200, 50, 50);
ellipse(200 + offset, 200, 50, 50);
rect(50 + offset, 160, 200, 20)
}
Listing 4-3Using variable
offset
变量续
我想用一个不同的例子来说明变量的另一种行为。我们只在屏幕中间画一个圆形,中间画一个矩形(列出 4-4 和图 4-3 )。
图 4-3
Output from Listing 4-4
function setup() {
createCanvas(800, 300);
rectMode(CENTER);
}
function draw() {
background(1, 186, 240);
// circle
fill(237, 34, 93);
noStroke();
ellipse(400, 150, 200, 200);
// rectangle
fill(255);
rect(400, 150, 150, 30);
}
Listing 4-4
Circle and rectangle
你能想出一个我们可以对上述程序进行的优化吗?注意我们是如何重复形状的 x 和 y 位置值的。让我们用一个变量来代替(清单 4-5 )。
function setup() {
createCanvas(800, 300);
rectMode(CENTER);
}
function draw() {
background(1, 186, 240);
// declaration of variables
var x = 400;
var y = 150;
// circle
fill(237, 34, 93);
noStroke();
ellipse(x, y, 200, 200);
// rectangle
fill(255);
rect(x, y, 150, 30);
}
Listing 4-5Using a variable to create a circle and rectangle
因为这些形状并不是相对于画布大小来定位的,所以如果我们要改变画布的大小,形状的相对位置也会改变。对于正方形画布,形状当前位于中心,但是对于更宽的画布,形状可能会开始向左侧倾斜。对于任何给定的画布大小,要使形状靠近中心,我们可以从使用变量设置画布的宽度和高度值开始。然后我们可以利用相同的变量来控制形状的位置。
在setup
函数中,我们将创建两个名为canvasWidth
和canvasHeight
的新变量,它们的值分别为 800 和 300。我们将把这些变量传递给createCanvas
函数,而不是使用之前的硬编码值。计划是我们也可以在draw
函数中使用这些相同的变量,这样即使我们要改变画布的大小,形状的相对位置也会保持不变。所以让我们在draw
函数中使用这些变量(清单 4-6 )。我们将它们除以 2,这样我们就可以得到画布宽度和高度的半个点。
function setup() {
var canvasWidth = 800;
var canvasHeight = 300;
createCanvas(canvasWidth, canvasHeight);
rectMode(CENTER);
}
function draw() {
background(1, 186, 240);
// declaration of variables
var x = canvasWidth/2;
var y = canvasHeight/2;
// circle
fill(237, 34, 93);
noStroke();
ellipse(x, y, 200, 200);
// rectangle
fill(255);
rect(x, y, 150, 30);
}
Listing 4-6Using variables in the
draw
function
当执行代码时,您会注意到我们得到一个错误。如果我们要查看控制台内部的错误消息,它说明了没有定义变量名:
Uncaught ReferenceError: canvasHeight is not defined (sketch: line 14)
Uncaught ReferenceError: canvasWidth is not defined (sketch: line 14)
这可能令人惊讶,因为我们已经在setup
函数中明确声明了这些变量。这个错误的原因与一个叫做scope
的东西有关。变量的作用域决定了在哪里可以访问变量。JavaScript 变量在使用var
关键字声明时有一个函数作用域。
您也可以使用'let'
和'const'
关键字来声明变量。使用这些关键字声明的变量有不同的相关作用域规则,但是出于本书的目的,我们不会深入研究这些关键字的用法。
函数作用域的工作原理是,在函数内部声明的任何变量在函数外部都是不可见的。它只对它所在的函数和可能嵌套在该函数中的其他函数可用。同样,如果我们在顶层有一个变量,这个变量将对该层和嵌套层中的所有内容可见,比如可能在那里定义的函数。我们现在面临的问题是在setup
函数中定义的变量在draw
函数中是不可见的。因此,如果要在draw
函数中声明变量,它们在同一级别的其他函数中是不可见的。
这个问题的解决方案是这样的:我们不应该在setup
函数中声明我们的变量,而是应该在这个顶层声明它们,这样它们就可以被在顶层声明的其他任何东西访问(清单 4-7 )。
// declaration of global variables
var canvasWidth = 800;
var canvasHeight = 300;
function setup() {
createCanvas(canvasWidth, canvasHeight);
rectMode(CENTER);
}
function draw() {
background(1, 186, 240);
// declaration of variables
var x = canvasWidth/2;
var y = canvasHeight/2;
// circle
fill(237, 34, 93);
noStroke();
ellipse(x, y, 200, 200);
// rectangle
fill(255);
rect(x, y, 150, 30);
}
Listing 4-7Declaring a global variable
在顶层声明的变量称为全局变量。在这个顶层声明变量通常不是最好的主意,因为我们在浏览器中运行我们的代码,而在浏览器中运行的其他东西,如插件、附加组件等。,可能会因针对不同用途定义同名变量而导致冲突。每当两个变量声明共享相同的名称时,后来声明的那个变量会覆盖另一个变量,因为代码是从上到下执行的。这可能会导致程序不按预期运行。但作为初学者,这不是你必须担心的事情。其他更有经验的开发人员——也有同样的顾虑——会有适当的保护措施来确保他们的变量不会被覆盖。现在,我们可以把我们的变量放在顶部,并且能够在相同级别或更低级别定义的不同函数中共享它们。
在这种情况下,我们在setup
函数之外初始化必要的变量,以便这些变量可以从setup
和draw
函数中访问。现在,我们可以尝试将canvasWidth
和canvasHeight
变量设置为不同的值,并注意形状如何始终保持在中心,因为它的位置是使用与画布相同的变量导出的。
p5.js 中预定义的变量
p5.js 是一个非常有用的库,它有几个预定义的变量,我们可以用它们来获得某些值。我们可以使用的两个这样的变量名是width
和height
。通过在setup
或draw
函数中使用这些变量名,我们可以得到当前的画布大小。这使得我们可以通过定义自己的变量名来做同样的事情。p5.js 开发人员一定已经意识到这是很多开发人员会尝试自己做的事情,因此提供了一个更简单的问题解决方案。
有了这些知识,清单 4-7 中的代码可以写成清单 4-8 中所示的形式。
function setup() {
createCanvas(800, 300);
rectMode(CENTER);
}
function draw() {
background(1, 186, 240);
// declaration of variables
var x = width / 2;
var y = height / 2;
// circle
fill(237, 34, 93);
noStroke();
ellipse(x, y, 200, 200);
// rectangle
fill(255);
rect(x, y, 150, 30);
}
Listing 4-8Working with predefined variables
你应该注意到width
和height
是 p5.js 变量,这意味着它们在setup
或draw
函数之外不可用。
现在我们知道如何使用变量,我们可以动画我们的形状!p5.js 中动画的诀窍是记住 p5.js 不断地为我们执行draw
函数。每次再次执行draw
函数时,我们放在该函数中的任何内容实际上都被重新绘制。
这个draw
功能被执行的次数(可以认为是渲染到屏幕上)被称为帧率。默认情况下,p5.js 的帧速率为 60。这意味着它每秒钟尝试重新绘制(或呈现)60 次draw
函数的内容。如果我们有办法改变这些draw
调用之间使用的变量的值,那么我们就能够创建动画。
这应该会让您想起动画书动画。每次调用一个draw
函数都会产生一个静态图像,但是由于它每秒发生 60 次,每个图像都略有不同,所以你会感觉它是动态的。
为了能够创建一个动画,我们将在名为count
的draw
函数之外初始化一个变量。在draw
函数中,我们将使用这个简单的表达式,每次调用draw
函数,变量count
就会增加 1。
count = count + 1;
现在,如果我们要在 position 参数中使用这个变量,我们可以移动形状(清单 4-9 )。这是我们 p5.js 冒险的一个惊人的进步。
var count = 0; // initialize a counter variable
function setup() {
createCanvas(800, 300);
rectMode(CENTER);
}
function draw() {
background(1, 186, 240);
// declaration of variables
var x = width / 2 + count;
var y = height / 2;
// circle
fill(237, 34, 93);
noStroke();
ellipse(x, y, 200, 200);
// rectangle
fill(255);
rect(x, y, 150, 30);
count = count + 1; // increment the counter variable
}
Listing 4-9.Animating a shape
如果我们不想让形状移动,而是想让它变大呢?放轻松!我们将首先创建一个size
变量,并在我们的形状中使用它,而不是硬编码的值,以便能够更容易地更新大小(清单 4-10 )。
var count = 0; // initialize a counter variable
function setup() {
createCanvas(800, 300);
rectMode(CENTER);
}
function draw() {
background(1, 186, 240);
// declaration of variables
var x = width / 2;
var y = height / 2;
var size = 200 + count; // control the size of the shapes
// circle
fill(237, 34, 93);
noStroke();
ellipse(x, y, size, size);
// rectangle
fill(255);
rect(x, y, size*0.75, size*0.15);
count = count + 1; // increment the counter variable
}
Listing 4-10Using a size variable
摘要
在这一章中,我们重温了以前见过的操作符,并谈了一点关于操作符优先的内容。然后我们又看了看变量,了解了更多关于它们的行为,尤其是关于它们的范围。我们还了解了 p5.js 自带的一些内置变量,比如只在setup
和draw
函数中可用的width
和height
。
最后我们创作了我们的第一部动画!
实践
创建一个动画,其中最初在屏幕外的五个矩形被动画化,从左侧进入屏幕,从右侧退出。它们也应该以不同的速度移动。
五、条件语句和比较运算符
在前一章中,我们看到了 p5.js 为我们提供的一些变量。需要注意的一点是,这些变量只能在 p5.js 函数setup
和draw
内部使用。如果我们试图在这些函数之外使用它们,我们会得到一个错误,说它们没有被声明。
在这一章中,我们将看看 p5.js 为我们提供的另一个有用的变量:frameCount
。我们还将学习frames
和frameRate
功能。
帧数、帧速率和帧
还记得我们在上一章是如何定义一个count
函数来计算draw
函数被调用的次数的吗?我们实际上可以使用 p5.js 为此提供的名为frameCount
的变量。frameCount
是一个变量,记录draw
函数在程序生命周期中被调用的次数。默认情况下,draw
功能每秒最多被调用 60 次。p5.js 内部一个名为frameRate
的设置决定了这个值。
这个变量的引入保证了关于 p5.js 中的frames
是什么的讨论。我们可以认为frame
是draw
函数调用的结果。draw
函数在一秒钟内被调用无数次,而frameRate
函数决定了调用的次数。如果我们调用不带参数的frameRate
函数,它将返回 p5.js 的当前frame rate
——我们可以将它保存到一个变量和console.log
中,以查看它在每一帧中的值(清单 5-1 )。
function setup() {
createCanvas(400, 400);
}
function draw() {
background(220);
console.log(frameRate());
}
Listing 5-1Console.log the frame rate
违约率在 60 左右。这意味着draw
功能每秒最多执行 60 次。这个数字取决于我们的系统资源。由于与性能相关的原因,例如有限的系统资源,可以达到的实际帧速率可能低于此目标值。我们可以认为 60 是 p5.js 努力达到的理想帧速率,但实际帧速率和性能可能低于这个值。
将框架想象成翻页书动画中的纸张。每秒查看的页面越多,动画就越流畅。这就是为什么高帧速率是可取的。如果帧速率较低,动画可能看起来参差不齐。我们可以通过向frameRate
函数传递一个整数值,在 p5.js 中显式地设置帧速率。值为 1 的frameRate
将每秒调用一次我们的draw
函数。
如果我们不想要任何动画,那么我们可以在setup
函数中调用一个名为noLoop
的函数。这个函数调用将导致 draw 函数只被调用一次。
总而言之,frameCount
是在程序的整个生命周期中执行draw
函数的次数。frameRate
是一秒钟内执行 draw 函数的次数。如果一个程序的frameRate
为 60,3 秒后的frameCount
将在60*3=180
左右。
如前所述,我们可以通过调用不带参数的frameRate
函数来查看当前的帧速率。但是我们实际上可以做得更好,并把结果显示在屏幕上,而不是把结果显示出来。
在 p5.js 中,我们可以使用text
函数向屏幕显示一个值。text
函数在 x 和 y 位置显示作为第一个参数给出的值,作为第二个和第三个参数提供(列表 5-2 和图 5-1 )。这样,我们可以更容易地在程序中可视化帧速率。请注意,在高帧速率下,实际结果将很难读取,因为它从一帧到另一帧波动很大。
图 5-1
Visualize the frame rate
function setup() {
createCanvas(800, 300);
textAlign(CENTER, CENTER);
}
function draw() {
background(220);
fill(237, 34, 93);
textSize(36);
// get the current frame rate as an integer.
var fps = parseInt(frameRate(), 10);
text("frameRate: " + fps, width/2, height/2);
}
Listing 5-2Visualize the frame rate
parseInt
是一个 JavaScript 函数,允许我们将十进制数转换成整数。它需要第二个参数来表示我们使用的基数(通常是 10)。
还要注意,在清单 5-2 中,我们使用了一个名为textAlign
的 p5.js 函数,带有参数CENTER
、CENTER
,能够在屏幕上水平和垂直对齐文本。否则,文本将从左上角开始绘制,而不是居中。
我们也可以尝试在屏幕上显示frameCount
变量(列表 5-3 )。如前所述,这是保存调用draw
函数的次数的变量。
function setup() {
createCanvas(800, 300);
textAlign(CENTER, CENTER);
}
function draw() {
background(220);
fill(237, 34, 93);
textSize(36);
text("frameCount: " + frameCount, width/2, height/2);
}
Listing 5-3Displaying the
frameCount
使用frameCount
变量,我们可以很快得到一个值,这个值随着draw
函数的每次执行而增加。请注意清单 5-4 中的内容,如果frameRate
较低,则frameCount
变量会变化得更慢。
function setup() {
createCanvas(800, 300);
textAlign(CENTER, CENTER);
frameRate(6); // make animation slower
}
function draw() {
background(220);
fill(237, 34, 93);
textSize(36);
text("frameCount: " + frameCount, width/2, height/2);
}
Listing 5-4Using the
frameRate
variable
我们可以重写上一章的例子,使用内置的frameCount
变量,而不是使用我们的count
变量(清单 5-5 )。
function setup() {
createCanvas(800, 300);
rectMode(CENTER);
}
function draw() {
background(1, 186, 240);
// declaration of variables
var x = width / 2;
var y = height / 2;
// increment the size with the current frameCount value
var size = 200 + frameCount;
// circle
fill(237, 34, 93);
noStroke();
ellipse(x, y, size, size
);
// rectangle
fill(255);
rect(x, y, size*0.75, size*0.15);
}
Listing 5-5Using the
frameCount
variable
条件式
到目前为止,我们编写的所有程序都是以自顶向下的线性方式执行的。但是在编程中,只有在满足特定条件时才执行程序的某些部分是很常见的。例如,使用变量frameCount
,我们现在能够在屏幕上制作一个形状的动画,但是如果我想让这个动画只在某一帧之后开始,比如在第 100 帧之后,该怎么办呢?
这可以使用一个叫做if
语句的编程结构来完成。if
语句允许我们仅在满足特定条件时才执行一段代码。一个if
语句是这样写的,我们从声明if
开始,在它旁边的括号内,我们写一个表达式,其值应该为true
或false
。接下来,在紧接在if
语句之后的花括号内,我们编写了一段代码,如果我们编写的表达式计算结果为true
,我们希望执行这段代码:
if (<conditional statement>) {
// do something
}
true
或false
是 JavaScript 中的实际值,就像数字是值一样。它们只是与Number
或String
不同类型的值。它们被称为Boolean
值或Boolean
数据类型。由于true
和false
是本地 JavaScript 数据类型,我们可以不加任何引号地输入它们,也不会出现错误:
console.log(true);
如果我们键入True
或False
(第一个字母大写),我们不能得到相同的结果。编程语言在你如何写东西方面是特别的。True
不等同于true
。此外,True
不是 JavaScript 识别的值,所以不加引号将导致错误:
console.log(True);
//Uncaught ReferenceError: True is not defined(...)
我们也可以使用comparison
操作符来生成true
或false
值。Comparison
运算符允许我们将两个值相互比较,结果,它们根据比较结果生成一个true
或false
值。以下是比较运算符的示例。我们用符号bigger-than
来比较两个数字,如果左边的数字比右边的大,就会返回true
;否则返回false
。
console.log(10 > 2); // would evaluate to true
console.log(1 > 100); // false
console.log(100 > 1); //true
如果左侧的值大于或等于右侧的值,则Bigger or equals >=
返回true
。
console.log(100 >= 100); //true
还有smaller <
和smaller or equals <=
比较运算符。
console.log(1 < 10); //true
console.log(10 <= 10); //true
为了比较两个值来检查它们是否相等,我们将使用三重等号===
。这不同于我们可能习惯的数学课,在数学课中,等号运算符是一个等号运算符=
。但是在 JavaScript 中,我们已经使用了单个等号运算符作为赋值操作。
console.log(1 === 1); //true
我们还可以进行比较,检查两个值是否不相等。为此,我们在等号前面用一个感叹号。
console.log(1 !== 1);
确保尝试使用我们所学的比较操作来查看它们在控制台中生成的结果。
让我们看看清单 5-6 和图 5-2 中利用if
结构的例子。
图 5-2
Output from Listing 5-6
var num;
function setup() {
num = 1;
createCanvas(800, 300);
textAlign(CENTER, CENTER);
}
function draw() {
background(220);
fill(237, 34, 93);
textSize(48);
if (num === 1) {
// this code gets executed only if num is equivalent to 1.
text('TRUE', width / 2, height / 2);
}
}
Listing 5-6Using the if structures
将执行if block
,因为括号内的表达式将计算为true
。毕竟第一就相当于第一。我们将在屏幕上看到单词 TRUE,因为这就是if block
中的代码所做的。
如果我们将num
变量的值改为 2,那么我们将看不到屏幕上显示的任何内容,因为这一次,if
块的比较结果将为false
,并且条件将不会被执行。
有一个额外的结构只能用于一个叫做else
模块的if
模块。一个else
块跟在一个if
块之后,并且对于没有被if
块覆盖的每一个其他比较都被执行。让我们使用一个else
模块来扩展前面的例子(列表 5-7 和图 5-3 )。
图 5-3
Output from Listing 5-7
var num;
function setup() {
num = 2;
createCanvas(800, 300);
textAlign(CENTER, CENTER);
}
function draw() {
background(220);
fill(237, 34, 93);
textSize(48);
if (num === 1) {
// this code gets executed only if num is equivalent to 1.
text('TRUE', width / 2, height / 2);
} else {
// this code gets executed if num is NOT equivalent to 1.
text('FALSE', width / 2, height / 2);
}
}
Listing 5-7Using an
else
block
现在在清单 5-7 的例子中,else
语句只有在if
语句没有被执行的时候才会被执行。这是针对每一个不为 1 的num
变量的值。
顺便说一下,注意我们是如何通过编写两次text
函数来重复自己的。我们可以refactor
我们的代码更简洁一点(列出 5-8 )。根据维基百科,重构是重新构造现有计算机代码的过程——改变分解——而不改变其外部行为。
var num;
function setup() {
num = 2;
createCanvas(800, 300);
textAlign(CENTER, CENTER);
}
function draw() {
var value;
background(220);
fill(237, 34, 93);
textSize(48);
if (num === 1) {
value = 'TRUE';
} else {
value = 'FALSE'
}
text(value, width/2, height/2);
}
Listing 5-8Refactoring our code
重构之前这段代码的问题是,如果我们想改变文本的位置,我们需要记住在两次text
函数调用中改变它。记住这样做似乎很容易,但是即使像这样的小事实际上也会使代码维护变得更加困难。
还有一个条件块可以添加到一个if
条件块中,那就是一个else if
块。一个else if
块将允许我们处理额外的条件。例如,在清单 5-9 中,我们可以给前面的例子添加几个else if
块:
var num;
function setup() {
num = 2;
createCanvas(800, 300);
textAlign(CENTER, CENTER);
fill(237, 34, 93);
}
function draw() {
var value;
background(220);
textSize(48);
if (num === 1) {
value = 'TRUE';
} else if (num === 2) {
value = 'STILL TRUE';
} else if (num === 3) {
value = 'YEP, TRUE';
} else {
value = 'FALSE'
}
text(value, width/2, height/2);
}
Listing 5-9Using the
else if
block
尝试更改num
变量的值,看看代码是如何运行的。使用else if
块,我们可以为num
的值处理两个更具体的条件。
利用我们所学的知识,让我们修改我们在上一章中编写的代码(清单 4-10 ,使动画的行为取决于frameCount
变量,如清单 5-10 所示。
var size;
function setup() {
createCanvas(800, 300);
rectMode(CENTER);
size = 200;
}
function draw() {
background(1, 186, 240);
// declaration of variables
var x = width / 2;
var y = height / 2;
var size = 200;
if (frameCount < 30) {
size = size + frameCount;
} else {
size = size + 30;
}
// ellipse
fill(237, 34, 93);
noStroke();
ellipse(x, y, size, size);
// rectangle
fill(255);
rect(x, y, size*0.75, size*0.15);
}
Listing 5-10Making the animation conditional
我们改变了前面的例子,如果frameCount
值小于 30,那么形状将使用frameCount
来制作动画;否则,它将保持静态。
我们还可以通过使用&&
或||
操作符将两个逻辑表达式组合在一起创建复合语句。&&
代表AND
。这允许我们编写这样的表达式,只有当条件语句的所有部分都是true
时,表达式的计算结果才是true
。假设我们想仅在frameCount
大于 20 AND
小于 30 的情况下制作形状动画。我们可以使用一个复合的and
语句来组合这两个条件(清单 5-11 )。
if (20 < frameCount && frameCount < 30) {
size = size + frameCount;
}
Listing 5-11Using a compound
and statement
||
代表OR
。OR
复合语句只要条件语句的一部分是true
就返回true
。如果frameCount
小于 30 OR
并且frameCount
值大于 120,假设我们想要制作这个形状的动画。为了表达这一点,我们可以编写清单 5-12 中所示的脚本。
if (frameCount < 30 || frameCount > 120) {
size = size + frameCount;
}
Listing 5-12Using a compound
or statement
摘要
在本章中,我们学习了框架的概念,以及它如何帮助我们在 p5.js 中创建动画图像。
我们还学习了 p5.js frameCount
变量,它记录了到目前为止显示了多少帧,以及frameRate
函数,它允许我们为 p5.js 设置帧速率。
我们学习了几个其他的 p5.js 函数,比如允许我们在屏幕上绘制文本的text
函数和允许我们对齐我们在屏幕上绘制的文本的textAlign
函数。
从 JavaScript 世界中,我们了解了比较运算符;Boolean
数据类型;true
和false
;最重要的是if
、else if
和else
条件句。这些结构通常在编程中使用,在许多其他编程语言中也可以找到。它们允许我们编写行为更加智能的代码,而不是自上而下盲目执行。
实践
创建一个动画,将最初在屏幕外的五个矩形动画化,使其从左侧进入屏幕。它们应该以不同的速度移动,并且应该在离开屏幕之前停下来。
六、更多 p5.js 变量
在前一章中,我们学习了 p5.js frameCount
变量,它为我们提供了一个数字,表示调用draw
函数的次数。我们可以在 p5.js 中使用很多其他非常有用的变量。我们将在本章中学习更多。
老鼠被压迫
mouseIsPressed
是我们将看到的第一个 p5.js 变量,它允许我们为程序添加一些交互性。mouseIsPressed
是一个 p5.js 变量,当鼠标点击画布区域时,该变量取值为true
,其他时间取值为false
。让我们改变第 4 (清单 4-10 )章中的一个例子,来快速看看我们如何使用这个变量(清单 6-1 )。
function setup() {
createCanvas(800, 300);
rectMode(CENTER);
}
function draw() {
background(1, 186, 240);
// declaration of variables
var x = width / 2;
var y = height / 2;
var size = 200; // control the size of the shapes
// circle
fill(237, 34, 93);
noStroke();
ellipse(x, y, size, size);
// conditionally display rectangle on mouse press
if (mouseIsPressed === true) {
fill(255);
rect(x, y, size*0.75, size*0.15);
}
}
Listing 6-1
Conditionally display
rectangle inside the circle
单击画布区域将会显示圆圈内的矩形。通过使用mouseIsPressed
p5.js 变量,我们使得矩形的显示以鼠标被按下为条件。
基于鼠标点击切换事物的状态可能是一个更复杂的例子,所以让我们看看如何解决这个问题。假设我们想在每次单击鼠标按钮时改变草图的背景颜色。在清单 6-2 中,我们将让它在两种颜色之间切换。
var toggle = true;
function setup() {
createCanvas(800, 300);
rectMode(CENTER);
}
function draw() {
// change the toggle value based on mouse press.
if (mouseIsPressed === true) {
toggle = !toggle;
}
// display a different bg color based on the toggle value
if (toggle === true) {
background(1, 186, 240);
} else {
background(250, 150, 50);
}
// declaration of variables
var x = width / 2;
var y = height / 2;
var size = 200;
// circle
fill(237, 34, 93);
noStroke();
ellipse(x, y, size, size);
// rectangle
fill(255);
rect(x, y, size * 0.75, size * 0.15);
}
Listgin 6-2
Toggle display
on mouse click
在这个例子中,我们正在创建一个名为toggle
的全局变量,它将存储一个Boolean
值。然后我们通过使用感叹号操作符,使这个Boolean
值改变为与每次鼠标点击相反的值。当感叹号用在一个Boolean
值的前面时,感叹号简单地反转该值,这意味着它会使true
变成false
,反之亦然。
您可能会注意到,mouseIsPressed
变量在捕捉我们的点击时似乎不太好用。这是因为draw
函数在一秒钟内被调用了无数次,这使得很难使用条件来检测鼠标点击。稍后,我们将看到使用 p5.js Events
检测鼠标点击的更好方法。
mouseX 和 mouseY
p5.js 变量mouseX
保存鼠标当前的水平位置,mouseY
保存当前的垂直位置。这听起来很简单,但是它们有可能在我们的程序中实现大量的用户交互,因此是非常有用的变量。如果我们将这些值作为一个形状的 x 和 y 坐标提供,那么当我们在屏幕上移动光标时,我们实际上也在移动这个形状。
让我们用之前程序的简化版本来尝试一下(清单 6-1 )。清单 6-3 和图 6-1 显示了它的一个版本,只是在屏幕中间画了一个圆。
图 6-1
Drawing a circle
function setup() {
createCanvas(800, 300);
}
function draw() {
background(1, 75, 100);
// declaration of variables
var x = width / 2;
var y = height / 2;
var size = 50;
// circle
fill(237, 34, 93);
noStroke();
ellipse(x, y, size, size);
}
Listing 6-3Drawing a simple
circle to the screen
现在让我们使用mouseX
和mouseY
变量来表示清单 6-4 中的 x 和 y 值。
function setup() {
createCanvas(800, 300);
}
function draw() {
background(1, 75, 100);
// declaration of variables
var x = mouseX;
var y = mouseY;
var size = 50;
// circle
fill(237, 34, 93);
noStroke();
ellipse(x, y, size, size);
}
Listing 6-4Using mouseX and mouseY variables
尝试在画布上移动鼠标。这不是很神奇吗?通过使用两个内置变量,我们将原本静态的草图变成了用户可以与之交互的东西。
你有没有想过为什么我们要在draw
函数中设置background
函数?我们似乎只需要设置这个值一次,所以你可能认为它应该去setup
函数。
将background
函数放在draw
函数中允许我们用纯色覆盖前一帧中绘制的所有内容。如果没有该声明,在该帧的开始,您会注意到来自前一帧的绘图会保留在屏幕上。但是对于某些用例,这可能正是您想要的。
清单 6-5 和图 6-2 显示了之前的示例(清单 6-4 ),具有更小的圆尺寸、更低的形状颜色不透明度,并且background
仅在setup
函数中声明一次。
图 6-2
Drawing onscreen using mouseX and mouseY variables
function setup() {
createCanvas(800, 300);
background(1, 75, 100);
}
function draw() {
// declaration of variables
var x = mouseX;
var y = mouseY;
var size = 25;
// circle
fill(237, 34, 93, 100);
noStroke();
ellipse(x, y, size, size);
}
Listing 6-5Persisting the drawing on the screen
摘要
在这一章中,我们学习了更多的 p5.js 内置变量,它们特别有助于我们创建交互式程序:可以响应用户动作的程序。
我们学习了 p5.js mouseIsPressed
变量,每当鼠标被点击时,该变量都假定一个true
值。但是我们也了解到这个变量可能不是处理用户输入的最佳方式。我们稍后将在 p5.js 中看到Event
s 的概念,它在处理用户输入方面要好得多。
我们还看到了mouseX
和mouseY
变量,以及如何使用它们来根据鼠标光标位置制作对象动画,这使我们能够以一种简单的方式为程序添加大量的交互性。
实践
构建一个脚本,在每次鼠标点击时,在屏幕上鼠标光标的位置绘制一个矩形。
七、循环
计算机最擅长的事情之一就是重复。想象一下,必须用不同的参数在屏幕上创建一千个形状。以我们目前的编程知识,这样做将花费我们不合理的时间。在这种情况下,我们希望按原样或有变化地重复我们的代码,我们可以利用一种叫做循环的编程结构。循环允许我们一遍又一遍地执行一段代码。
我们已经熟悉了 p5.js 中的循环概念。如果你仔细想想,draw
函数是一个不断重复执行的循环,直到我们退出 p5.js 程序。在本章中,我们将学习如何自己构建这种循环。
For 循环
JavaScript 中有几种不同类型的循环结构,但目前最流行的是for loop
。它允许我们重复一定次数的操作。一个for loop
有四个部分。清单 7-1 提供了一个如何构建for loop
的例子。
for (var i = 0; i < 10; i = i + 1) {
//do something
}
Listing 7-1Example of a for loop
在第一部分中,我们初始化一个变量,该变量将记录循环执行的次数——我们称之为计数器变量。
var i = 0;
按照惯例,在for loop
内部,我们通常倾向于使用像i
或j
这样的短变量名,尤其是如果这个变量只用于控制for loop
的流量。但是,如果对您的用例有意义,也可以随意使用其他名称。
在第二部分中,我们为我们的循环定义了一个测试条件,每次循环将要开始时都会对其进行评估。在这个例子中,我们检查计数器变量是否小于数字 10。
i < 10;
在第三部分中,我们定义了一种方法来更新在循环结束时计算的计数器变量。在这个例子中,我们得到变量i
的当前值,并给它加 1。
i = i + 1;
最后,在花括号内,我们写了我们想要重复的代码。一旦计数器变量不满足测试条件,循环就终止,程序返回到正常求值。
如果测试条件从未失败,那么我们将有一个循环,它将最终创建一个infinite loop
,一个没有退出条件的循环,这样它将一直继续下去,直到程序被外部手段终止。p5.js 中的draw
函数处于无限循环中;它会一直绘制到屏幕上,直到我们关闭浏览器窗口。
尽管无限循环是一种有效的用例,但循环最常用于执行已知次数的操作。让我们创建一个循环,它将使用for loop
在屏幕上绘制给定数量的椭圆(列表 7-2 和图 7-1 )。
图 7-1
Output for Listing 7-2
function setup() {
createCanvas(800, 300);
}
function draw() {
background(1, 75, 100);
// circle properties
fill(237, 34, 93);
noStroke();
for (var i=0; i<10; i=i+1) {
ellipse(0, 0, 50, 50);
}
}
Listing 7-2Create ellipses
using a for loop
在我们的例子中,我们在屏幕上画了 10 个圆,但是没有办法在视觉上进行区分,因为所有的圆都是一个接一个地画出来的。这就是利用循环计数器变量的意义所在。我基本上可以在每次调用循环时使用这个变量来偏移圆圈的位置(列表 7-3 和图 7-2 )。
图 7-2
Output for Listing 7-3
function setup() {
createCanvas(800, 300);
}
function draw() {
background(1, 75, 100);
// circle properties
fill(237, 34, 93);
noStroke();
for (var i=0; i<10; i=i+1) {
ellipse(i * 50, 0, 50, 50);
}
}
Listing 7-3Using a loop counter in a
for loop
在输入椭圆函数之前,我们将循环变量乘以 50(圆的直径)。这使得我们的形状不会互相重叠。
现在,如果我们要执行这个,我们将会看到for loop
为我们创建的所有这些圆圈。最棒的是,因为我们构建了重复操作的结构,所以扩展它就像将循环中使用的数字改为更大的值一样简单。渲染 100 或 1000 个圆而不是 10 个,只是改变这一个值的问题。然而,如果我们开始使用巨大的数字,我们可能会开始注意到性能下降。
让我们构建我们的代码,以便我们可以用圆形填充屏幕的整个宽度(列表 7-4 和图 7-3 )。
如果屏幕的宽度是 800,一个圆的直径是 50 个单位,那么这将意味着我们可以将800 / 50
个圆填充到页面的宽度中。我们会注意到在页面的末尾有一点空隙,因为第一个圆圈在画布外面一点。我们可以通过在 x 位置上加 25 来抵消所有的东西来消除这个间隙,这是直径值的一半。正如您已经知道的,我们实际上不需要自己做这些计算,因为我们可以让 JavaScript 为我们计算这个值。
此时您可能会注意到,我们将大量值硬编码到代码中,为了灵活性,最好使用变量。为此,我们将重构我们的代码。
图 7-3
Output for Listing 7-4
function setup() {
createCanvas(800, 300);
}
function draw() {
background(1, 75, 100);
// circle properties
fill(237, 34, 93);
noStroke();
var diameter = 50;
for (var i=0; i< width/diameter; i=i+1) {
ellipse(diameter/2 + i * diameter, 0, diameter, diameter);
}
}
Listing 7-4Filling the screen width
with circles
现在,如果我们要改变一个值,即圆的直径,整个代码仍然会画出刚好足够填满屏幕的圆。这是一件令人印象深刻的事情。
如果我们也想用圆形填充屏幕的高度呢?为了做到这一点,我们需要编写另一个 for 循环,为画布的整个长度放置圆,为宽度放置每个圆。这要求我们在第一个循环中放置第二个循环,有效地将一个循环嵌套在另一个循环中。见清单 7-5 和图 7-4 。
图 7-4
Output for Listing 7-5
function setup() {
createCanvas(800, 300);
}
function draw() {
background(1, 75, 100);
// circle properties
fill(237, 34, 93);
noStroke();
var diameter = 50;
for (var i=0; i<width/diameter; i=i+1) {
for (var j=0; j<height/diameter; j=j+1) {
ellipse(
diameter/2 + i * diameter,
diameter/2 + j * diameter
,
diameter,
diameter
);
}
}
}
Listing 7-5Filling the screen with circles
注意我们在这个例子中声明ellipse
函数的方式。我们将它写在多行上,以增加可读性。JavaScript 不关心空格,所以使用多行代码不会导致任何错误。
这段代码现在非常有用。首先,它很健壮;我们可以更改绘图区域的大小或正在绘制的圆的数量,但事情仍然会继续正常运行。
需要记住的是:由于需要执行的操作数量,将循环放在另一个循环中会使我们的程序非常慢。此外,有时嵌套结构也会使我们的程序难以阅读。
随机和噪声函数
由于我们现在可以创建每次执行时都使用不同值的循环,这可能是学习 p5.js random
函数的好时机。p5.js random
函数每次被调用时都会生成一个随机数。当我们想要为正在绘制的形状的参数使用随机值时,这很有用。
如果我们在没有任何参数的情况下调用random
函数,那么对于每个draw
函数调用或每个帧,都会产生一个介于 0 和 1 之间的随机数。如果我们给random
函数提供一个值,那么它将返回一个大于 0 小于给定值的随机值。如果我们给random
函数提供两个值,那么我们将得到一个介于给定的两个数之间的随机值。这些情况的例子见清单 7-6 。
console.log(random()); // a random number in between 0 and 1
console.log(random(10)); // a random number in between 0 and 10
console.log(random(100, 1000)); // a random number in between 100 and 1000
Listing 7-6Examples of using the
random function
清单 7-7 是一个以不同方式使用random
函数的小脚本。图 7-5 显示了该脚本的结果。显示的数字是随机生成的,每次执行代码时都会有所不同。
图 7-5
Output from Listing 7-7
function setup() {
createCanvas(800, 300);
textAlign(CENTER, CENTER);
fill(237, 34, 93);
frameRate(1);
}
function draw() {
var random_0 = random();
var random_1 = random(10);
var random_2 = random(100, 1000);
var offset = 40;
textSize(24);
background(255);
text(random_0, width/2, height/2-offset);
text(random_1, width/2, height/2-0);
text(random_2, width/2, height/2+offset);
}
Listing 7-7Using the
random function
有了清单 7-8 和图 7-6 ,让我们更新我们之前的代码(清单 7-5 )来使用random
函数。
图 7-6
Output from Listing 7-8
function setup() {
createCanvas(800, 300);
}
function draw() {
background(1, 75, 100);
// circle properties
fill(237, 34, 93);
noStroke();
var diameter = 50;
for (var i=0; i<width/diameter; i=i+1) {
for (var j=0; j<height/diameter; j=j+1) {
ellipse(
diameter/2 + i * diameter,
diameter/2 + j * diameter,
diameter * random(), // using the random function
diameter
);
}
}
}
Listing 7-8Using the
random function
我们使用random
函数的结果,将椭圆的宽度乘以一个随机数,每次调用random
函数时,这个随机数都是 0 到 1 之间的一个值。由于random
函数可以在任何帧中取其范围内的任何值,所以动画看起来相当激进。如果我们想要随机性逐渐变化,因此看起来更有机一点,那么我们应该研究一下noise
函数。
我们可以向noise
函数输入任何数值,它将返回一个介于 0 和 1 之间的半随机值。对于给定值,它总是返回相同的输出。关于noise
函数的好处是,如果我们提供给noise
函数的值只是递增地变化,那么输出值也只会递增地变化。这将导致我们得到的随机值之间的平滑过渡。
为了能够概念化noise
函数是如何工作的,我们可以认为无限数量的随机值像波浪一样逐渐变化,我们提供给noise
函数的值就像这些随机值的坐标。本质上,我们只是对一个已经存在的噪声进行采样。每当我们为噪声函数提供相同的值时,我们将收到相同的半随机值作为回报。
我们将重写上面的程序(列表 7-8 )来使用noise
函数。我们将为noise
函数提供frameCount
变量,因为这是在 p5.js 中获取序列号的好方法。但是我们将frameCount
除以 100,以便能够减缓值的变化,从而稍微减缓最终的动画。见清单 7-9 和图 7-7 。
图 7-7
Output from Listing 7-9
function setup() {
createCanvas(800, 300);
}
function draw() {
background(1, 75, 100);
// circle properties
fill(237, 34, 93);
noStroke();
var diameter = 50;
for (var i=0; i<width/diameter; i=i+1) {
for (var j=0; j<height/diameter; j=j+1) {
ellipse(
diameter/2 + i * diameter,
diameter/2 + j * diameter,
diameter * noise(frameCount/100), // using then noise function
diameter * noise(frameCount/100) // using then noise function
);
}
}
}
Listing 7-9Using the
noise function
注意现在所有的形状是如何使用相同的动画的。如果我们想为这些形状中的每一个获得不同的噪波值呢?目前我们有重复的值,因为当提供相同的值时,noise
函数返回相同的输出。为了能够为每个形状获得不同的输出值,我们可能想要重写上面的函数,以利用for loop
的i
和j
值来调整噪声的采样位置。见清单 7-10 和图 7-8 。
图 7-8
Output from Listing 7-10
function setup() {
createCanvas(800, 300);
}
function draw() {
background(1, 75, 100);
// circle properties
fill(237, 34, 93);
noStroke();
var diameter = 50;
for (var i=0; i<width/diameter; i=i+1) {
for (var j=0; j<height/diameter; j=j+1) {
ellipse(
diameter/2 + i * diameter,
diameter/2 + j * diameter,
// applying a different animation to each circle
diameter * noise(frameCount/100 + j*10000 + i*10000),
// applying a different animation to each circle
diameter * noise(frameCount/100 + j*10000 + i*10000)
);
}
}
}
Listing 7-10Applying a different animation to each circle
我们上面用作乘数的值10000
完全是任意的。我们只是试图确保我们提供给noise
函数的坐标彼此相距较远。
摘要
循环是编程中最强大的结构之一。它们让我们能够利用计算机的真正计算能力,在更大范围内重复人类在合理时间内不可能完成的操作。
在这一章中,我们学习了如何构建for loops
以及如何将循环嵌套在一起,以获得重复形状的网格,而不仅仅是一行。
我们还学习了 p5.js random
和noise
函数以及它们之间的区别。
实践
创建一个循环,该循环将创建一个颜色从黑色逐渐变为白色的矩形数组(图 7-9 )。你应该以这样一种方式构建循环,即单个变量将控制绘制的矩形的数量。
图 7-9
Practice image