p5.js 学习手册(全)
一、简介
在我们生活的这个时代,编码是无价的。它有能力提升你的职业生涯,你的未来前景,甚至你的智力。计算正在推动历史上最大的资本扩张之一,而现在是学习编码的最佳时机。
为什么要学编码?
我第一次认真接触编程是在大学。我们必须学习一门叫做 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
八、函数
函数是 JavaScript 的主要构件。它们允许我们以更有效和可伸缩的方式编写程序。函数通过在单个可执行名称下包含和分组操作来帮助我们管理复杂性。我们已经知道如何使用 p5.js 预定义的函数如ellipse
或background
来调用函数。我们甚至将自己的函数声明为 p5.js 迫使我们将代码放入两个函数声明中:setup
和draw
。如果我们想要创建我们自己的函数,我们将遵循我们一直用于创建或声明这些函数的相同约定。
创建函数
为了创建(或声明)一个新的函数,我们将从使用function
关键字开始,然后给函数起一个我们选择的名字,这个名字理想地描述了函数的行为或目的。见清单 8-1 。
function functionName() {
// function body
}
Listing 8-1Creating a function
在函数名的旁边,我们会打开括号。如果我们想要构建一个处理用户输入的函数,我们可以在括号内定义参数,作为将来用户输入的占位符变量名。我们稍后会看到这是如何工作的。
然后我们有花括号。在花括号内可以称为函数体。在那里,我们编写构建函数逻辑的代码。我们还可以使用参数,即我们在函数名旁边的括号中定义的变量名,作为我们希望在函数体内执行的操作的一部分。
让我们看一个简单的例子。注意 p5.js 有一个ellipse
函数,但没有一个circle
函数。这不是一个真正的问题,因为我们可以通过提供具有相同宽度和高度值的ellipse
函数来创建一个圆。不过,为了便于讨论,让我们创建一个使用三个值的circle
函数:我们想要画圆的x
和y
位置以及圆的直径。
清单 8-2 展示了如何去做。在括号内,我们将写下变量名,这些变量名最终将在调用该函数时提供。这些名称被称为参数,因为它们参数化了我们正在创建的操作的函数。我们将在函数中使用这些参数,以允许用户控制函数的内部工作。
function circle(x, y, diameter) {
ellipse(x, y, diameter, diameter);
}
Listing 8-2Declaring a circle function
我们可以选择任何东西作为参数名,但是使用能清楚表达意图的名称通常是有意义的。所以在我们的例子中,使用名字x
、y
和diameter
是有意义的。
在定义了这个函数之后,我们可以通过使用它的名字并为它提供值来调用它。提供给函数的值称为函数的参数。请注意,如果没有提供所有必需的参数,函数可能会失败或无法按预期工作(清单 8-3 )。
circle(width/2, height/2, 100);
Listing 8-3Calling the circle function
如果你觉得术语很混乱,不要太担心。可能需要一段时间来适应。函数的参数可以被认为是用户在使用函数时最终提供的值。调用函数时提供的那些相同的值被称为参数。
有了circle
函数,我们再也不用担心用ellipse
函数画圆了。我们可以用自己的函数画出完美的圆形。在我们自己实现了circle
函数之后,我们知道它实际上使用了底层的ellipse
函数来绘制那些圆。但是函数的巧妙之处在于,一旦它们对我们可用,我们就不需要知道它们是如何工作的。我们可以直接使用它们,而不用考虑它们是如何实现的。由创建 p5.js 的聪明人实现的ellipse
函数可能会使用里面的各种东西来绘制椭圆,但就我们而言,它在被调用时会绘制椭圆,这才是最重要的。
在这个例子中,创建一个circle
函数并没有给我们带来太多的效率。事实上,我们可以只传递三个参数给ellipse
函数,而不是四个参数来画一个圆。但是当我们构建更复杂的程序时,函数变得非常重要。它们通过在单个可执行名称下包含和分组操作来帮助我们管理复杂性。函数本质上是黑盒。它们封装了内部包含的代码。此外,在函数内部使用var
关键字声明的任何变量在函数外部都是不可见的。这意味着从定义它们的函数外部调用它们会导致错误。参见清单 8-4 中的示例。
function setup() {
createCanvas(800, 300);
sayHello();
}
function draw() {
background(220);
}
function sayHello() {
var message = 'Hello World!';
console.log(message);
}
console.log(message); // this line will throw an error
Listing 8-4
Variable visibility
(scope)
第 15 行的console.log
函数将抛出一个错误,因为变量message
只在函数sayHello
内部可见。
函数可以在没有输入、单个输入或多个输入的情况下工作;它们要么返回结果,要么不返回。让我解释一下返回值的含义。
假设我们想要创建一个给定数值乘以自身的函数,本质上是计算给定数值的平方。清单 8-5 显示了一个这样做的函数。它接收一个数字作为参数,并创建在屏幕上显示该数字的文本。因为我们可以使用这个函数在屏幕上显示一个数字的平方,所以这个函数有点用处。结果如图 8-1 所示。
图 8-1
Output from Listing 8-5
function setup() {
createCanvas(800, 300);
}
function draw() {
background(1, 75, 100);
squared(10);
}
function squared(num) {
fill(237, 34, 93);
textSize(60);
textAlign(CENTER, CENTER);
text(num * num, width/2, height/2);
}
Listing 8-5Creating a multiplying function
但是如果我们想在另一个计算中使用这个结果,我们可能会遇到障碍。这个函数不会将数字返回给我们;它只是把它显示在屏幕上。调用这个函数会影响我们所处的环境,但是它不会返回一个值来用于进一步的计算。到目前为止,我们看到的一些函数,如ellipse
、rect
等,以类似的方式运行,它们做一些事情,但实际上并不返回值作为计算的结果。然而,random
函数在执行时不会在屏幕上显示任何内容,而是返回一个我们可以在变量中捕获的值。
为了能够从函数返回值,我们可以使用return
关键字。让我们将squared
函数改为两者:在屏幕上显示结果,并且返回一个值(清单 8-6 )。
function setup() {
createCanvas(800, 300);
}
function draw() {
background(1, 75, 100);
var x = squared(10);
console.log(x);
}
function squared(num) {
fill(237, 34, 93);
textSize(60);
textAlign(CENTER, CENTER);
var result = num * num;
text(result, width/2, height/2);
// return the value of the result from the function
return result;
}
Listing 8-6Using the return keyword
现在,这个函数返回一个我们在console.log
函数中使用的值。每当程序遇到关键字return
时,程序就会终止函数的执行,并将它旁边声明的值返回给函数的调用者。这意味着如果我们在return
关键字下面有任何其他行,它们不会被执行,因为return
终止了当前函数的执行。return
关键字只在函数内部可用。试图从函数外部使用它会导致错误。因为在函数之外,没有什么要返回的。
重温设置和绘图函数
既然我们已经学习了如何创建函数,强调声明函数和调用函数之间的区别是很重要的。请注意,当我们创建函数时,为了执行它们,我们必须调用它们。例如,在清单 8-7 中的代码示例中,我们只创建或声明了一个函数:
function myFunction() {
}
Listing 8-7Creating and declaring a function
为了能够使用这个函数,我们需要执行它,通过用它的名字调用它并在名字旁边使用括号,如清单 8-8 所示。
myFunction();
Listing 8-8Calling a function
注意在 p5.js 中工作时有一点有点奇怪。我们从来没有真正调用过setup
和draw
函数,然而它们还是被执行了!这是由 p5.js 的架构决定的。p5.js 为我们处理setup
和draw
函数的执行,因为它们的执行遵循一些简单的规则,如下所示:
setup
函数在draw
函数之前执行。setup
函数只执行一次,而draw
函数以一定的默认速率连续执行。
摘要
从我们开始使用 p5.js 的那一刻起,我们就在使用函数。它自己的架构依赖于我们程序中两个函数的存在,这两个函数的名字必须是setup
和draw
。而且,我们一直在使用 p5.js 库自带的函数,比如ellipse
、rect
等。
我们已经看到,函数可以被构建为使用或不使用外部用户输入。我们也可以构建使用或者不使用return
关键字返回值的函数。
函数是创建模块化代码块的一种方式,这些代码块可以在我们的代码中重用。这些函数通过减少我们需要编写的代码量,使我们的程序更易于维护和扩展。每当我们发现自己在多个地方重复一个代码块时,它很可能是创建一个函数的好选择。
实践
创建一个名为grid
的函数,它将使用三个参数:一个numX
和一个numY
参数,它将在 x 轴上创建numX
数量的形状(比如矩形),在 y 轴上创建numY
数量的形状,还有一个size
参数,它将设置形状的大小。
例如:
grid(10, 30, 20); // Would create 10 x 30 rectangles of size 20px.
九、对象
JavaScript 包含一个名为Objects
的数据结构。Objects
帮助你组织代码,在某些情况下,它们使工作变得更容易。创建对象有两种方式:使用object initializer
或使用constructor functions
。在这一章中,我们将使用一个对象初始化器创建一个单独的对象,而构造函数作为一个蓝图,我们可以使用new
关键字创建许多对象实例。
使用Object Initializer
JavaScript 使用一种叫做Object
的数据结构来帮助组织数据。在 JavaScript 中有几种创建object
的方法。一种方法是使用花括号,如清单 9-1 所示。
var colors = {};
Listing 9-1Creating an object with curly brackets
这些花括号叫做Object Initializer
。他们创建了一个空对象。我们通过使用变量colors
来保存对对象的引用。
现在,我们可以通过在一个点之后提供所需的属性名来为这个colors
对象添加属性。这被称为点符号。我们还将为这些新创建的属性赋值。见清单 9-2 。
var colors = {};
colors.black = 0;
colors.darkGray = 55;
colors.gray = 125;
colors.lightGray = 175;
colors.white = 255;
console.log(colors);
Listing 9-2Adding properties
to an object
如果我们在这一点上用console.log
来看这个物体,我们会看到它看起来像这样:
{"black":0,"darkGray":55,"gray":125,"lightGray":175,"white":255}
我们也可以从一开始就创建一个具有相同属性的对象,通过在花括号内提供这些属性(清单 9-3 )。
var colors = {
black: 0,
darkGray: 55,
gray: 125,
lightGray: 175,
white: 255,
};
console.log(colors);
Listing 9-3Adding properties inside the curly brackets
对象基本上是键值对。每个键存储一个值,每个键-值对组成一个对象的属性。
如清单 9-4 所示,要访问对象上的值,我们可以再次使用点符号。
console.log(colors.gray);
Listing 9-4Access a value
of an object
在某些情况下,点符号不起作用。一个例子是当我们在一个对象中使用数字作为键值时。在这种情况下,我们可以使用方括号来访问值。参见清单 9-5 。
console.log(colors[1]); // Assuming you were using numbers instead of color names as key values.
Listing 9-5Use square brackets to access values
如果我们对上面的表达式进行console.log
处理,你认为会得到什么结果?我们将得到值undefined
,因为键 1 在我们当前的colors
对象中不存在。
我们也可以将函数定义为对象中的键值。在这种情况下,产生的属性将被称为方法。
从我们的colors
对象继续,让我们在该对象内部定义一个名为paintItBlack
的方法,这将使背景颜色为黑色(清单 9-6 )。
var colors = {
black: 0,
darkGray: 55,
gray: 125,
lightGray: 175,
white: 255,
paintItBlack: function() {
background(this.black);
}
};
Listing 9-6Defining a method
清单 9-7 显示了一个使用这个对象的 p5.js 代码。
var colors;
function setup() {
createCanvas(800, 300);
colors = {
black: 0,
darkGray: 55,
gray: 125,
lightGray: 175,
white: 255,
paintItBlack: function() {
background(this.black);
}
};
}
function draw() {
background(220);
// calling the paintItBlack method after frame 120.
if (frameCount > 120) {
colors.paintItBlack();
}
}
Listing 9-7Using an object
在这个例子中,我们在setup
和draw
函数范围之外初始化colors
变量,然后在setup
函数中创建它的内容。毕竟我们只需要一次创作的内容。如果frameCount
大于 120,我们就调用它的paintItBlack
方法,这在默认设置下会在两秒后发生。(记住frameRate
的默认值是 60,这意味着每秒大约渲染 60 帧。)
为了能够使用在对象内部定义的键,我们需要能够引用对象本身。在 JavaScript 中,有一个叫做this
的关键字,允许我们这样做(列举 9-8 )。使用this
关键字,我们可以引用定义在对象本身上的键。
paintItBlack: function() {
background(this.black);
}
Listing 9-8Using the
this keyword
一旦我们在对象上定义了一个方法,我们就可以通过使用点符号访问它来调用这个方法(清单 9-9 )。因为我们在这个实例中执行的是一个函数,所以函数名后面需要有括号。
colors.paintItBlack();
Listing 9-9
Calling the method
JavaScript(或其他实现对象的语言)中存在对象的概念,因此我们可以模仿现实世界中的对象或概念。就像现实世界中的对象有属性,有时有行为一样,编程语言对象可以有描述它们是什么的属性和指定它们如何行为的方法。
通过清单 9-10 ,让我给你一个模仿现实世界概念的编程语言对象的例子。我们将创建一个名为circle
的对象。这个circle
对象将有几个定义其外观的属性,并且它将有几个描述其行为的方法。
var circle = {
x: width/2,
y: height/2,
size: 50,
};
Listing 9-10Creating an object
这个circle
对象有一个定义其坐标的x
和y
属性以及一个定义其大小的size
属性。我们还将在它上面创建一个方法,一个作为函数的属性,它定义了某种行为(清单 9-11 )。在这种情况下,定义的行为将是在屏幕上画圆。
var circle = {
x: width/2,
y: height/2,
size: 50,
draw: function() {
ellipse(this.x, this.y, this.size, this.size);
},
};
Listing 9-11Adding a draw method to the circle object
在这个例子中,我们再次使用this
关键字来访问对象的属性。this
关键字基本上指的是对象本身,允许我们在对象内部调用对象的属性。我们现在可以通过使用circle.draw()
方法调用在屏幕上画这个圆:
circle.draw();
你一定在想:这是有史以来最令人费解的事情。因为当我们可以调用一个函数在屏幕上画一个圆的时候,为什么我们还需要这样画一个圆呢?
ellipse(width/2, height/2, 50, 50);
Listing 9-12Using the ellipse function
to draw a circle to the screen
不过,我们才刚刚开始。让我们给这个圆添加另一个名为grow
的方法,每当它被调用时,这个方法都会将圆的大小增加一个单位(清单 9-13 )。
var circle = {
x: width/2,
y: height/2,
size: 50,
draw: function() {
ellipse(this.x, this.y, this.size, this.size);
},
grow: function() {
if (this.size < 200) {
this.size += 1;
}
},
};
Listing 9-13Adding
grow method
现在,如果我们要在draw
函数中调用这个函数,我们会看到我们的循环随着 p5.js 不断调用draw
函数而不断增长。清单 9-14 提供了完整的例子。图 9-1 显示了结果输出。
图 9-1
Output from Listing 9-14
var circle;
function setup() {
createCanvas(800, 300);
circle = {
x: width/2,
y: height/2,
size: 50,
draw: function() {
ellipse(this.x, this.y, this.size, this.size);
},
grow: function() {
if (this.size < 200) {
this.size += 1;
}
},
};
}
function draw() {
background(220);
// circle properties
fill(237, 34, 93);
noStroke();
circle.draw();
circle.grow();
}
Listing 9-14Using the circle object
如前所述,对象的使用是关于代码组织的。我们没有单独的函数来操纵这个圆,但是我们有一个circle
对象来携带这些函数和它自身的属性。在某些情况下,这可以使我们的代码更容易推理。
使用构造函数
在 JavaScript 中还有另一种创建对象的方法,那就是使用函数(清单 9-15 )。我们在创建函数的对象内部所做的声明与我们在使用对象初始化器时所做的非常相似。注意我们是如何在函数中使用width
和height
p5.js 变量的。为了让这些变量对这个函数可用,需要在createCanvas
函数之后调用它。
var Circle = function() {
this.x = width/2;
this.y = height/2;
this.size = 50;
this.draw = function() {
ellipse(this.x, this.y, this.size, this.size);
};
this.grow = function() {
if (this.size < 200) {
this.size += 1;
}
};
};
Listing 9-15Using a function to create an object
一个创建对象的函数叫做constructor function
。我们可以把它看作是创建新对象的模板或蓝图,这些新对象从这个构造函数中派生出它们的属性。
清单 9-16 展示了一个例子来更好地解释我的意思。假设我们想要一个像前面例子中一样的圆,展示由这个Circle
构造函数定义的行为。在这种情况下,我们不会直接使用这个构造函数,但是我们将使用它来实例化一个模仿这个模板函数的新圆。
var myCircle = new Circle();
Listing 9-16Using a constructor function
我们使用了Circle
构造函数和new
关键字来创建一个名为myCircle
的圆的新实例,它从构造函数中获取属性。基本上,new
关键字允许我们从构造函数中创建一个对象的新实例。我们可以把Circle
构造函数想象成一个蓝图,而myCircle
则是根据这个蓝图构建的一个实际的圆。现在我们可以通过调用它的draw
方法将这个新创建的圆绘制到屏幕上(清单 9-17 )。
myCircle.draw();
Listing 9-17Calling the draw method
清单 9-18 提供了完整的示例。
var circle;
function setup() {
createCanvas(800, 300);
// instantiating a new circle using the Circle Constructor Function
circle = new Circle();
}
function draw() {
background(220);
// circle properties
fill(237, 34, 93);
noStroke();
circle.draw();
circle.grow();
}
var Circle = function() {
this.x = width/2;
this.y = height/2;
this.size = 50;
this.draw = function() {
ellipse(this.x, this.y, this.size, this.size);
};
this.grow = function() {
if (this.size < 200) {
this.size += 1;
}
};
};
Listing 9-18Using a constructor function
这种方法的美妙之处在于,我们可以从同一个蓝图中不断创造新的圆圈。由于这些圆是独立的实体或实例,它们可以具有彼此不同的属性。让我们看看清单 9-19 和图 9-2 中的例子。
图 9-2
Output from Listing 9-19
var circle_1;
var circle_2;
var circle_3;
function setup() {
createCanvas(800, 300);
// instantiating circles
circle_1 = new Circle();
circle_2 = new Circle();
circle_3 = new Circle();
}
function draw() {
background(220);
// circle properties
fill(237, 34, 93);
noStroke();
circle_1.draw();
circle_1.grow();
circle_2.x = 150;
circle_2.draw();
circle_2.grow();
circle_3.x = 650;
circle_3.draw();
circle_3.grow();
}
var Circle = function() {
this.x = width / 2;
this.y = height / 2;
this.size = 50;
this.draw = function() {
ellipse(this.x, this.y, this.size, this.size);
};
this.grow = function() {
if (this.size < 200) {
this.size += 1;
}
};
};
Listing 9-19Creating separate circle instances
在这个例子中,我们在 p5.js 函数之外创建了三个变量,分别叫做circle_1
、circle_2
和circle_3
。这些变量是在 p5.js 函数之外创建的,因此它们在这两个函数的范围内。
我们通过使用new
关键字将Circle
构造函数赋给这些变量,使它们成为Circle
实例。现在我们有了三个独立的圆形对象,我们可以在draw
函数中改变它们的属性,并且我们从每个对象中得到不同的结果。
需要注意的一点是,我们如何使用以大写字母开头的函数名作为构造函数。我们用一个大写字母来提醒自己和他人,这个函数是一个构造函数,需要用new
关键字来调用。
用关键字new
调用构造函数是很重要的。如果我们不这样做,它就不能正常工作,因为构造函数中的关键字this
不会引用实例对象,而是引用全局对象。
大写字母的用法不是规则,而是惯例。没有人强迫我们去做。但是我们应该遵循这个约定,因为没有实现一个函数是一个构造函数,然后在没有关键字new
的情况下调用它将会产生意想不到的后果。
摘要
在这一章中,我们学习了 JavaScript 对象。简单地说,对象是组织代码的一种方式。有两种创建对象的方法。一种方法是使用一个object initializer
,另一种方法是使用constructor functions
。
我们还学习了用于访问对象属性的点符号和方括号符号。关键字this
允许我们从对象本身内部引用对象的属性。
在不同的编程语言中有一个完整的编程范例,叫做面向对象编程,它利用对象来组织代码并使代码清晰。使用 p5.js,我们不一定需要创建对象来组织我们的代码,但是我想引入对象有两个原因:
- 它们是 JavaScript 语言的基础部分。如果你想在以后学习更多的语言,你需要熟悉对象是如何工作的。
- JavaScript 还有其他基于我们将要使用的对象的内置结构,所以进一步熟悉对象对我们来说很重要。
十、数组
Arrays
是 JavaScript 中另一个有用的数据结构。它们是用编号索引存储的数据的顺序集合,并且基于Objects
,这使得某些操作更容易执行。
在本章中,我们将使用push
方法填充一个数组。我们还将学习remainder
操作符,我们可以用它来导出在零和期望值之间循环的连续值。
使用push
方法
请记住,我们使用花括号来创建一个空对象。我们可以使用方括号以类似的方式创建一个空数组(清单 10-1 )。
var arr = [];
Listing 10-1Create an empty array
在本例中,我们创建了一个空数组,并使用一个名为arr
的变量来存储该数组。现在,如果我们想向这个数组添加元素,我们可以使用数组对象拥有的push
方法(清单 10-2 )。
var arr = [];
arr.push(1);
arr.push("hello world");
arr.push({"name":"value"});
console.log(arr);
Listing 10-2Adding elements
to the array
在本例中,我们将三个新值推送到先前的空数组中。在第一行中,我们将一个number
类型的值推入数组,在第二行中,我们将一个string
类型推入数组,在第三行中,我们将一个object
类型推入数组。
现在,如果我们使用console.log
来查看数组的内容,我们将在屏幕上看到类似这样的内容:
[1,"hello world",{"name":"value"}]
注意我们是如何使用不同的数据类型和对象来填充数组的。数组可以包含任何对象,甚至其他数组。就像 JavaScript 对象一样,我们可以在创建时填充数组,方法是在方括号内提供所需的值,并用逗号分隔它们。让我们创建一个包含四个数字的数组(清单 10-3 )。
var arr = [15, 40, 243, 53];
console.log(arr);
Listing 10-3Creating an array with different data types
我们可以使用自动生成的索引号属性来访问数组中的各个项。不过,有一点要知道,引用数组中存储项的索引是从 0 开始计数的。要访问数组中的单个项,我们可以键入存储数组的变量名,然后使用方括号中的索引号来引用该索引处的项(参见清单 10-4 )。数字 0 表示数组中的第一项——15,索引号 1 表示第二项,依此类推
var arr = [15, 40, 243, 53];
var firstItem = arr[0];
console.log(firstItem);
Listing 10-4Accessing the items of an array
如果我们试图访问一个不存在的项目,我们将得到一个undefined
值。这是有意义的,因为该项没有定义。记住,当我们试图访问一个不存在的属性时,对象也会返回一个undefined
值。
让我们看看数组数据结构如何在构建程序时简化事情。我们将从一个简单的例子开始(列出 10-5 )。假设我们想要创建五个不同大小的不同圆。要用我们目前的知识做到这一点,我们需要创建五个不同的变量,并为这些变量赋予所需的值。然后我们调用ellipse
函数五次,每次使用不同的变量。
var size1 = 200;
var size2 = 150;
var size3 = 100;
var size4 = 50;
var size5 = 25;
function setup() {
createCanvas(800, 300);
}
function draw() {
// circle properties
fill(237, 34, 93);
strokeWeight(2);
ellipse(width/2, height/2, size1, size1);
ellipse(width/2, height/2, size2, size2);
ellipse(width/2, height/2, size3, size3);
ellipse(width/2, height/2, size4, size4);
ellipse(width/2, height/2, size5, size5);
}
Listing 10-5Drawing circles of different sizes
我们只在屏幕上画了五个圆圈,但这已经看起来像是一个麻烦的解决方案。如果我们需要画 100 个甚至 1000 个圆呢?这就是数组发挥作用的地方,它使我们的工作变得更加容易。
首先,让我们创建一个所需的圆形大小的数组。如前所述,我们可以使用索引号来访问数组中的各个项目。我们将使用这些知识从数组中获取所需的值。参见清单 10-6 。
var sizes = [200, 150, 100, 50, 25];
function setup() {
createCanvas(800, 300);
}
function draw() {
// circle properties
fill(237, 34, 93);
strokeWeight(2);
ellipse(width/2, height/2, sizes[0], sizes[0]);
ellipse(width/2, height/2, sizes[1], sizes[1]);
ellipse(width/2, height/2, sizes[2], sizes[2]);
ellipse(width/2, height/2, sizes[3], sizes[3]);
ellipse(width/2, height/2, sizes[4], sizes[4]);
}
Listing 10-6Using an array to store the size values
这已经看起来好多了。但是请注意重复发生的次数。当调用ellipse
函数时,我们实际上是在一遍又一遍地输入同样的东西;唯一改变的是指数。这里出现了一个非常清晰的模式:如果我们有一个结构,它会创建一个循环,让我们用递增的值调用ellipse
函数五次,那么我们就不必重复。
幸运的是,我们知道如何创建一个 for 循环来帮助我们做到这一点。清单 10-7 提供了上面重写的使用 for 循环的代码。
var sizes = [200, 150, 100, 50, 25];
for (var i = 0; i < 5; i++) {
ellipse(width / 2, height / 2, sizes[i], sizes[i]);
}
Listing 10-7A for-loop snippet
清单 10-8 和图 10-1 显示了 p5.js 示例中代码的用法:
图 10-1
Circles drawn using a for loop
var sizes = [200, 150, 100, 50, 25];
function setup() {
createCanvas(800, 300);
}
function draw() {
// circle properties
fill(237, 34, 93);
strokeWeight(2);
for (var i = 0; i < 5; i++) {
ellipse(width / 2, height / 2, sizes[i], sizes[i]);
}
}
Listing 10-8Entire code using for loop
注意到for loop
标题中数字 5 的用法了吗?它在那里是因为我们使用的数组中有五个元素。因此,如果有 6 个项目,那么我们应该将这个值更新为 6。但这有点问题;如果我们把数组做得更大,但是忘记更新这个值,会怎么样?幸运的是,我们可以使用一个名为length
的数组属性,它将给出数组中的项数。我们可以重写上面的代码来利用length
属性(清单 10-9 )。
var sizes = [200, 150, 100, 50, 25];
function setup() {
createCanvas(800, 300);
}
function draw() {
// circle properties
fill(237, 34, 93);
strokeWeight(2);
for (var i = 0; i < sizes.length; i++) {
ellipse(width / 2, height / 2, sizes[i], sizes[i]);
}
}
Listing 10-9Using the array height property
我们的代码现在简洁多了,而且可伸缩性也非常好。我们只需不断向sizes
数组添加新值,就会为我们画出等量的圆。只是为了好玩,让我们进一步自动化这个设置。目前,我们正在手动创建具有大小值的数组。但是我们可以创建另一个for loop
,通过使用random
函数用我们选择的任意数量的随机数填充这个数组(参见清单 10-10 和图 10-2 )。
图 10-2
Output from Listing 10-10
var sizes = [];
function setup() {
createCanvas(800, 600);
noFill();
// populating the sizes array with random values
for (var i=0; i<100; i++) {
var randomValue = random(5, 500);
sizes.push(randomValue);
}
}
function draw() {
background(255);
for (var i = 0; i < sizes.length; i++) {
ellipse(width / 2, height / 2, sizes[i], sizes[i]);
}
}
Listing 10-10Using the
random function
让我们看看这个例子中发生了什么。首先,我们在draw
函数中设置背景颜色为白色。此外,我们正在调用noFill
函数,它将绘制没有填充颜色的形状。这些只是风格上的选择。我们正在创建一个空的sizes
数组,我们将用随机数填充它。然后,我们创建一个循环,将迭代 100 次。在这个循环中,对于每次迭代,我们使用random
函数创建一个介于 5 和 500 之间的随机值,并使用push
方法将生成的随机值放入sizes
数组中。
下一步不变。我们正在为存在于sizes
数组中的所有值创建椭圆。注意改变这个程序中的一个值,产生的随机数的数量,现在是 100,控制了整个结果。这是一个很好的例子,展示了简单的编程结构如何创建非常健壮和可伸缩的解决方案。
使用数组
让我们使用数组来实现另一个可视化!计划是创建一个动画,连续不断地以一种风格的方式显示给定的单词。
首先,让我们复习一下如何在 p5.js 中创建文本。我们将使用带三个参数的text
函数:要显示的文本,以及该文本的 x 和 y 位置。利用这些知识,让我们在屏幕上以浅色背景显示单词“JavaScript”(参见清单 10-11 和图 10-3 )。
图 10-3
Output from Listing 10-11
function setup() {
createCanvas(800, 300);
}
function draw() {
background(200);
text('JavaScript', width/2, height/2);
}
Listing 10-11Using the
text fucntion
请注意,我们创建的文本没有垂直对齐。看起来不居中。使用 p5.js 中一个名为textAlign
的函数很容易解决这个问题(清单 10-12 )。只需在setup
函数中调用这个函数,向它传递值CENTER
。这将负责垂直对齐。我们可以再一次将CENTER
传递给这个函数来水平对齐文本。
textAlign(CENTER, CENTER);
Listing 10-12Using the textAlign function
接下来,让我们格式化文本,使它看起来更好一点。在清单 10-13 中,我们使用textSize
函数将文本大小设置为 45 像素,并使用fill
函数将文本颜色设置为白色(结果见图 10-4 )。
图 10-4
Output for Listing 10-13
function setup() {
createCanvas(800, 300);
textAlign(CENTER, CENTER); // centering the text
}
function draw() {
background(200);
fill(255); // text color
textSize(45); // text size
text('JavaScript', width/2, height/2);
}
Listing 10-13Using textAlign and styling the text
完美!在这个例子中,我们想要创建一个单词数组,并不断地循环遍历它们。让我们首先创建我们将使用的阵列。我们将在draw
函数之外创建它,因为我们只需要创建这个数组一次。如果我们在draw
函数中声明它,那么它会随着对draw
函数的每次调用而不断地被创建和销毁(默认情况下,每秒钟大约发生 60 次!).
让我们在draw
和setup
函数之外创建一个名为words
的变量(清单 10-14 )。因为变量是在setup
和draw
函数之外初始化的,所以它可以在这两个函数中使用。
var words = ['I', 'love', 'programming', 'with', 'JavaScript'];
Listing 10-14Creating a words variable
接下来,我们需要设计一种方法来连续生成一个介于 0 和数组长度之间的值,以便能够引用数组中的各个项。为此,我们可以使用remainder
( %
)操作符。
使用remainder
操作符
remainder
操作符与我们之前见过的所有操作符都有点不同,比如加号或减号,所以看看它是如何工作的可能会有所帮助。给定两个值,remainder
运算符返回第一个值除以第二个值后的余数。%
操作符象征着它。
正如我们在清单 10-15 中看到的,给定一个递增的第一个值,remainder
操作符允许我们循环遍历第二个值减一。
console.log(1 % 6) // returns 1.
console.log(2 % 6) // returns 2.
console.log(3 % 6) // returns 3.
console.log(4 % 6) // returns 4.
console.log(5 % 6) // returns 5.
console.log(6 % 6) // returns 0.
console.log(7 % 6) // returns 1.
// etc..
Listing 10-15Remainder operator
你可能会发现自己在想:“你怎么会知道这些?”因为,如果我们只知道remainder
操作符做了什么,但没有任何使用它的实践,这可能是很难想到的事情。这是完全正常的。通过看到其他人使用操作符或结构,您可以了解为了某种目的可以使用哪种操作符或结构。有时这是经验和实践的问题,而不是知识的问题。
如果我要向一个remainder
操作符提供一个恒定的增量值以及数组的长度,我将能够生成在 0 和那个长度之间循环的值。
在 p5.js 上下文中,不断提供的值可以是frameCount
变量。记住frameCount
告诉我们到目前为止draw
函数被调用了多少次。如清单 10-16 所示,让我们在draw
函数中创建一个名为currentIndex
的变量,它使用remainder
操作符、frameCount
p5.js 变量和单词数组的长度来创建介于 0 和数组长度减 1 之间的值。
var currentIndex = frameCount % words.length;
Listing 10-16Using the remainder operator
我们可以console.log
这种说法来验证我们确实在期望的范围内创造价值。但是更好的方法可能是使用text
函数,我们已经用 p5.js 显示了这个值。
在这一点上要注意的一件事是,数字的显示实在是太快了;真的很难理解发生了什么。我们应该放慢 p5.js 的速度,否则我们的文本将很难阅读。一种方法是使用frameRate
功能降低帧速率。如清单 10-17 所示,让我们将设置函数中的frameRate
值改为 3。结果如图 10-5 所示。
图 10-5
Output from Listing 10-17
var words = ['I', 'love', 'programming', 'with', 'JavaScript'];
function setup() {
createCanvas(800, 300);
textAlign(CENTER, CENTER);
frameRate(3); // using a lower frame rate to slowdown the text
}
function draw() {
var currentIndex = frameCount % words.length;
background(200);
fill(255);
textSize(45);
text(currentIndex, width/2, height/2);
}
Listing 10-17Slowing down the frameRate
太棒了。使用这个代码,我们应该能够看到一系列的数字显示在屏幕上。但是我们对在屏幕上显示数字不感兴趣——而是对数组中的单词感兴趣。利用我们的知识,这很容易做到。我们将使用方括号符号来访问数组中的各个项。
如清单 10-18 所示,让我们创建另一个变量currentWord
。该变量将存储由currentIndex
变量确定的当前单词。现在我们可以用这个变量代替text
函数中的currentIndex
。
var currentWord = words[currentIndex];
Listing 10-18Creating variable
currentWord
我们差不多完成了。但是我想做的另一件事是改变每个单词的背景颜色,因为现在这一点也不美观。
我们将创建另一个名为colors
的数组,它将包含颜色信息。原来我们可以把一个数组传入 p5.js 颜色函数,和把值一个一个传进去是一样的。
因此,如清单 10-19 所示,这两个表达式将创建彼此相同的颜色。
fill(255, 0, 0);
fill([255, 0, 0]);
Listing 10-19Using an array as a value for the fill function
我们将创建包含我们将使用的颜色数组的colors
数组。我们可以尝试自己想出颜色值,但是那样很难找到好看的颜色。
Adobe 有一个名为 Adobe Color CC ( https://color.adobe.com
)的网页,在那里我们可以找到在设计中使用的颜色主题。我会用它来找到一个与我的想象相匹配的主题。
在 Adobe Color CC 的“浏览”选项卡下,您可以选择所需的主题。将鼠标悬停在您想要的主题上,然后单击“编辑副本”这将引导您进入一个页面,在这里您可以看到这些颜色的 RGB 值。清单 10-20 是从该网站挑选的颜色样本。
var colors = [
[63, 184, 175],
[127, 199, 175],
[218, 216, 167],
[255, 158, 157],
[255, 61, 127],
];
Listing 10-20Color samples from Adobe Color CC
请注意,我的数据格式有点不同,因为我不想让行太长,因为这会影响代码的可读性。这只是一种风格上的选择。
现在我们可以在fill
函数中使用这些颜色值来改变每一帧背景的颜色。清单 10-21 展示了最终代码的样子。
var words = ['I', 'love', 'programming', 'with', 'JavaScript'];
var colors = [
[63, 184, 175],
[127, 199, 175],
[218, 216, 167],
[255, 158, 157],
[255, 61, 127],
];
function setup() {
createCanvas(800, 300);
textAlign(CENTER, CENTER);
frameRate(3); // using a lower frame rate to slowdown the text
}
function draw() {
var currentIndex = frameCount % words.length;
var currentColor = colors[currentIndex];
var currentWord = words[currentIndex];
background(currentColor);
fill(255);
textSize(45);
text(currentWord, width / 2, height / 2);
}
Listing 10-21Final Code
摘要
在本章中,我们学习了一种叫做数组的 JavaScript 数据结构。数组允许我们以连续的方式存储任意类型的多个值。存储在数组中的值可以使用方括号符号来访问。
我们可以使用push
方法,在数组第一次创建时或创建后用所需的值填充数组。数组在与循环一起使用时特别有用。循环让我们以一种非常简单的方式访问数组中的条目。
我们还学习了remainder
操作符。余数运算符返回两个数之间除法运算的余数。使用这个运算符,我们可以导出在零和期望值之间循环的连续值。
实践
构建一个名为countdown
的函数,它将获得两个参数——一个数字和一个消息—(清单 10-22 ),并将创建一个类似于上面的可视化效果,它将显示从给定数字到数字 0 的倒计时。在倒计时结束时,它应该在屏幕上显示给定的消息,即第二个参数。
您可以随意为该函数添加另一个参数,该参数将控制每个数字在屏幕上停留的时间。
countdown(10, "Launch!");
Listing 10-22
.
十一、事件
在第六章中,我们学习了一个叫做mouseIsPressed
的 p5.js 变量,当鼠标被按下时,它假定值为true
,而对于所有其他情况,它假定值为false
。
我们还了解到,这并不是一种捕捉用户输入的好方法,因为draw
函数的执行速度会使这个变量很难以可靠的方式更新。在这一章中,我们将回顾在 p5.js 中处理用户输入的其他方法,也就是解决这个问题的事件。使用事件,我们可以在draw
函数循环之外捕获用户输入。
在 p5.js 中有许多事件函数,我们可以声明它们来利用事件系统。这里我们将关注两个事件函数:mousePressed
和keyPressed
事件函数。
使用鼠标按下
这个想法类似于draw
和setup
函数,我们用这个特殊的名字声明这个函数,p5.js 以一种特殊的方式处理它(就像setup
和draw
函数一样)。
在 p5.js 代码中,我们在名称mousePressed
下声明的函数在每次按下鼠标按钮时被触发。让我们重写之前的例子,使用变量mouseIsPressed
来使用mousePressed
事件函数(清单 11-1 )。
var toggle = true;
function setup() {
createCanvas(800, 300);
rectMode(CENTER);
}
function draw() {
// 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;
if (frameCount < 60) {
size = size + frameCount;
} else {
size = size + 60;
}
// circle
fill(237, 34, 93);
noStroke();
ellipse(x, y, size, size);
// rectangle
fill(255);
rect(x, y, size*0.75, size*0.15);
}
function mousePressed() {
toggle = !toggle; // change the toggle value to be opposite.
}
Listing 11-1Using mousePressed event function
嗯,这是一个简单的重构!我们只是声明了一个我们自己不执行的函数。每当相应的动作发生时,执行由 p5.js 处理。
还有很多其他的事件函数。完整列表可在 https://p5js.org/reference/#group-Events
找到。
使用按键
另一个值得学习的事件函数是keyPressed
函数。顾名思义,每当按下一个键时,keyPressed
功能就会被触发。在清单 11-2 中,让我们在一个全新的草图中快速测试一下它是如何工作的。
function setup() {
createCanvas(800, 300);
}
function draw() {
background(220);
}
function keyPressed() {
console.log('pressed');
}
Listing 11-2Using the keyPressed function
在这个例子中,每次我们按下一个键,我们都会在控制台上看到一条消息“pressed”。在清单 11-3 中,让我们看一个更复杂的例子,每次按一个键都会在画布中创建一个形状。
var pressed;
function setup() {
createCanvas(800, 300);
background(220);
}
function draw() {
if (pressed === true) {
ellipse(
random(width),
random(height),
50,
50
);
}
pressed = false;
}
function keyPressed() {
pressed = true;
}
Listing 11-3Drawing a shape
with every keypress
这些形状是在我们按下一个键后创建的(图 11-1 )。
图 11-1
Output from Listing 11-3
注意一些事情。首先,我们把background
函数移到了setup
函数下面。这是为了确保我们绘制的形状保留在屏幕上。如果我们有一个在draw
函数中调用的background
函数,那么它会覆盖所有东西,覆盖每一帧,这对于这个用例来说是不理想的。此外,我们将ellipse
函数调用分布在几行代码中,同样是为了增加可读性。
我们有一个全局变量叫做pressed
。每按一次键,我们就将这个全局变量的值设置为true
。当这种情况发生时,draw
函数会在屏幕上显示一个ellipse
,因为条件语句已经执行。然后draw
函数立即将pressed
值再次设置为false
,这样我们只得到一个椭圆。
在清单 11-4 中,我们将对这个例子做一点改进,使它看起来更顺眼。目前,圆圈看起来有点太均匀,颜色有点太暗。我们将这样做,每次我们创建一个圆,它使用一个 0 到 200 之间的随机大小和一个预定义随机颜色列表中的随机颜色(图 11-2 )。
图 11-2
Output from Listing 11-4
var pressed;
var colors = [];
function setup() {
createCanvas(800, 300);
background(0);
colors = [
[245, 3, 155],
[13, 159, 215],
[148, 177, 191],
[100, 189, 167],
[242, 226, 133],
[176, 230, 110],
[123, 90, 240]
];
}
function draw() {
noStroke();
if (pressed === true) {
var randomIndex = parseInt(random(colors.length), 10); // convert the given number to an integer
var randomSize = random(200);
fill(colors[randomIndex]);
ellipse(
random(width),
random(height),
randomSize,
randomSize
);
}
pressed = false;
}
function keyPressed() {
pressed = true;
}
Listing 11-4Changing size and color
为了能够在每次按键时选择一种随机颜色,我们需要生成一个介于 0 和 colors 数组长度减 1 之间的随机integer
。我们使用负 1,因为数组索引从 0 开始计数。
要生成 0 和数组长度减 1 之间的任意随机数,我们可以简单地将random
函数写成random(
colors.length)
。这将最终生成一个介于 0 和之间的数,直到达到colors
数组中的项目数(不包括该数)。然而,问题是生成的数字是浮点数,这意味着它有小数位。然而,我们需要一个整数来访问数组中的条目。所以我们需要把十进制数转换成整数。有几种方法可以解决这个问题。一种方法是使用 p5.js floor
函数,它将给定的浮点数下舍入到最接近的整数。另一个解决方案是使用名为parseInt
的原生 JavaScript 函数,它将给定的值转换成整数——如果该值可以转换的话。我们不能指望向它抛出一个字符串名称值,然后接收一个整数。
如清单 11-5 所示,我们需要向parseInt
函数传递第二个参数来设置计算将要发生的基数。这个基数几乎总是 10。在浮点数上使用parseInt
函数看起来像这样。
var num = parseInt(0.5, 10);
console.log(num); // will be 0.
Listing 11-5Using parseInt on a float number
然而,识别被按下的键只是问题的一部分。我们应该能够做的另一件事是识别用户按下了哪个按钮。在keyPressed
函数中,理论上我们可以通过使用keyCode
变量来识别任何被按下的键。一个keyCode
变量以编码的方式保存用户按下的最后一个键,这样如果用户按下键‘a’,它将返回值‘65’,代表‘b’;66 '等…
由于 p5.js 是一个有用的库,这使得通过为它们提供预定义的变量来识别一些键变得更加容易,例如:BACKSPACE
、DELETE
、ENTER
、RETURN
、TAB
、ESCAPE
、SHIFT
、CONTROL
、OPTION
、ALT
、UP_ARROW
、DOWN_ARROW
、LEFT_ARROW
、RIGHT_ARROW
。
例如,清单 11-6 提供了一小段代码,每当‘Enter’键被按下时,它就执行一个console.log
语句。
function setup() {
createCanvas(800, 300);
}
function draw() {
background(220);
}
function keyPressed() {
if (keyCode === ENTER) {
console.log('Enter Pressed');
}
}
Listing 11-6Using keyCode values
使用keyCode
变量,我们可以通过一点解码来识别哪个字母数字键被按下。但是还有另一个特别适合字母数字字符的变量,叫做key
。key
变量按原样存储被按下的字母数字键的值,这样更容易识别哪个键被按下了。
摘要
在这一章中,我们学习了一种更好的处理事件的方法,那就是事件函数。我们特别关注两个事件函数:mousePressed
和keyPressed
事件函数。
我们还学习了一些可以在keyPressed
函数中使用的变量:key
和keyCode
。使用key
使得识别字母数字按键更容易,而keyCode
对于检测其他按键是理想的,因为它可以与 p5.js 变量进行比较,如ENTER
、SPACE
等。这使得识别这些按钮更加容易。
从 JavaScript 部分,我们了解了可以用来将类似数字的值(也包括表示数字的字符串)转换成整数的parseInt
函数。
实践
在屏幕上画一个矩形,键盘箭头键可以控制矩形的位置。
十二、关于 p5.js 的更多信息
此时,我们几乎已经准备好进行我们的最终项目了:一个使用 JavaScript 和 p5.js 构建的交互式游戏!那是在下一章。在此之前,我想演示几个更有用的 p5.js 函数来扩展我们可以构建的东西的领域。
你有没有注意到我们如何利用现有的知识在屏幕上画出形状,但是我们不能真正地变换它们,比如围绕它们的中心旋转它们?这是我们可以构建的视觉效果的一大障碍,所以在这一章中,让我们学习如何在 p5.js 中进行转换来增强我们的能力。
旋转和平移
在使用过其他类型的绘图库之后,我应该说在 p5.js 中进行缩放、旋转和平移形状之类的转换可能有点不直观。清单 12-1 和 12-2 是演示如何使用 p5.js rotate
函数的示例,该函数允许我们旋转形状。
图 12-1
Output for Listing 12-1
function setup() {
createCanvas(800, 300);
rectMode(CENTER);
noStroke();
}
function draw() {
background(220);
fill(237, 34, 93);
rect(width/2, height/2, 50, 50);
rect(width/2+50, height/2+50, 50, 50);
}
Listing 12-1Drawing rectangles
without rotation
目前,我们正在绘制两个彼此对角的矩形(图 12-1 )。让我们利用rotate
函数来看看会发生什么。
function setup() {
createCanvas(800, 300);
rectMode(CENTER);
noStroke();
}
function draw() {
background(220);
fill(237, 34, 93);
rotate(5);
rect(width/2, height/2, 50, 50);
rect(width/2+50, height/2+50, 50, 50);
}
Listing 12-2Using the
rotate function
你会注意到两个形状都从屏幕上消失了。如果您预期形状仅移动 5 度,这一定是一个令人困惑的结果。这是因为rotate
函数在 p5.js 中的默认单位是弧度。我们可以通过使用带有DEGREES
p5.js 变量的angleMode
函数来使这个函数使用度数。如清单 12-3 所示,在setup
函数中做这个声明。
angleMode(DEGREES);
Listing 12-3Using
angleMode
现在事情以或多或少符合预期的方式运行。我们现在可以观察到,当我们调用rotate
函数时,我们最终会旋转函数调用后出现的每个形状(清单 12-4 和图 12-2 )。
图 12-2
Output from Listing 12-4
function setup() {
createCanvas(800, 300);
rectMode(CENTER);
noStroke();
angleMode(DEGREES);
}
function draw() {
background(220);
fill(237, 34, 93);
rotate(5);
rect(width/2, height/2, 50, 50);
rect(width/2+50, height/2+50, 50, 50);
}
Listing 12-4Using rotate with angleMode
另一件要注意的事情是,旋转发生在原点周围,即画布的左上角。然而,当我们控制形状时,我们通常喜欢让它们绕着原点旋转。所以这个功能,照现在的样子,好像不是特别有用。
为了更好地控制rotate
函数,我们应该研究一下translate
函数。translate
功能将对象从原点移动给定的 x 和 y 平移量。在清单 12-5 中,让我们在当前的设置中使用它。结果如图 12-3 所示。
图 12-3
Output from Listing 12-5
function setup() {
createCanvas(800, 300);
rectMode(CENTER);
noStroke();
angleMode(DEGREES);
}
function draw() {
background(220);
fill(237, 34, 93);
translate(150, 0); // using translate function
rotate(5);
rect(width/2, height/2, 50, 50);
rect(width/2+50, height/2+50, 50, 50);
}
Listing 12-5Using the
translate function
现在发生的是translate
函数将画布内的所有内容向右移动 150 像素。它移动整个坐标系,因为旋转也围绕原点右侧的 150px 发生,而不是从原点开始。
事不宜迟,列表 12-6 和图 12-4 是关于如何围绕原点旋转事物。我认为展示它是如何做的比解释它更容易。我们现在将使用单一的形状。
图 12-4
Output from Listing 12-6
function setup() {
createCanvas(800, 300);
rectMode(CENTER);
noStroke();
angleMode(DEGREES);
}
function draw() {
background(220);
fill(237, 34, 93);
// rotating the shape around it's origin
translate(width/2, height/2);
rotate(45);
rect(0, 0, 100, 100);
}
Listing 12-6Rotating around the origin
在这个例子中,我们像往常一样绘制一个形状,但是使用translation
函数来设置它的 x 和 y 坐标,而不是将这些值直接输入到形状绘制函数中。这样做,当结合使用rectMode
功能时,允许我们绘制中心位于原点的形状。基本上,我们从在原点绘制形状开始,因为所有的变换函数都相对于该点起作用。然后我们使用translate
和rotate
功能将形状移动到想要的位置和角度。使用这种方法,我们需要记住在translate
函数后调用rotate
,否则旋转仍然会相对于原始原点发生,这可能不是我们想要的。
一般来说,这种方法和使用转换函数的缺点是,从这一点开始,我们绘制的所有东西都将使用这个新的原点。解决这个问题的方法是使用push
和pop
函数。
推动和弹出
p5.js push
函数允许我们创建一个新的状态,而pop
函数将状态恢复到以前的状态。这允许我们对单个对象应用完全不同的设置,而不用担心这些设置是否会影响后面的形状,只要我们在push
和pop
调用之间做所有的事情。同样,在示例中更容易看出这一点(列表 12-7 和图 12-5 )。
根据我们当前的设置,我们在translate
和rotate
函数后绘制的所有东西都将应用 45 度旋转。
fgura 12-5
Output from Listing 12-7
function setup() {
createCanvas(800, 300);
rectMode(CENTER);
noStroke();
angleMode(DEGREES);
}
function draw() {
background(220);
translate(width/2, height/2);
rotate(45);
// pink rectangle
fill(237, 34, 93);
rect(0, 0, 150, 150);
// white rectangle
fill(255, 255, 255);
rect(0, 0, 75, 75);
}
Listing 12-7Translate function with multiple shapes
在清单 12-8 中,让我们在这里实现push
和pop
函数,这样我们就可以隔离我们正在应用于更大矩形的变换。结果见图 12-6 。
图 12-6
Output for Listing 12-8
function setup() {
createCanvas(800, 300);
rectMode(CENTER);
noStroke();
angleMode(DEGREES);
}
function draw() {
background(220);
// translation and rotation will be contained in between
// push and pop function calls.
push();
translate(width/2, height/2);
rotate(45);
// pink rectangle
fill(237, 34, 93);
rect(0, 0, 150, 150);
pop();
// white rectangle
fill(255, 255, 255);
rect(0, 0, 75, 75);
}
Listing 12-8Using the push and pop functions
太棒了!无论我们在push
和pop
函数之间做什么,都不会影响这些函数调用之外的任何东西。需要注意的是,我们总是一起调用push
和pop
函数。使用一个而不使用另一个没有任何意义。
在清单 12-9 中,让我们更新我们的例子,这样我们仍然可以将粉色矩形平移到中间,但是对它应用不同的旋转值。
function setup() {
createCanvas(800, 300);
rectMode(CENTER);
noStroke();
angleMode(DEGREES);
}
function draw() {
background(220);
push();
translate(width/2, height/2);
rotate(45);
// pink rectangle
fill(237, 34, 93);
rect(0, 0, 150, 150);
pop();
push();
translate(width/2, height/2);
rotate(30);
// white rectangle
fill(255, 255, 255);
rect(0, 0, 75, 75);
pop();
}
Listing 12-9Applying different translations to different shapes
如果您发现自己希望 p5.js 转换没有这么复杂,您可以尝试构建自己的函数来处理和抽象掉复杂性。清单 12-10 提供了一个示例矩形函数,它接受第五个参数,即旋转参数。
function rectC(x, y, width, height, rotation) {
if (rotation === undefined) {
rotation = 0;
}
push();
translate(x, y);
rotate(rotation);
rect(0, 0, width, height);
pop();
}
Listing 12-10Declaring a custom function to handle transformations
这里,我们正在创建名为rectC
的矩形绘制函数,它包装了原始的rect
函数,但是在内部使用push
和pop
来保存状态和设置转换,并且它接受一个可选的旋转参数。如果没有提供旋转参数,那么它将假设值为undefined
。如果是这样的话,我可以将旋转值设置为 0。清单 12-11 是前一个例子的重构,以利用这个函数。请注意,这次它更简洁了。
function setup() {
createCanvas(800, 300);
rectMode(CENTER);
noStroke();
angleMode(DEGREES);
}
function draw() {
background(220);
// pink rectangle
fill(237, 34, 93);
rectC(width/2, height/2, 150, 150, 45);
// white rectangle
fill(255, 255, 255);
rectC(width/2, height/2, 75, 75, 30);
}
function rectC(x, y, width, height, rotation) {
// if rotation value is not provided assume it is 0
if (rotation === undefined) {
rotation = 0;
}
push();
translate(x, y);
rotate(rotation);
rect(0, 0, width, height);
pop();
}
Listing 12-11Using a custom function to handle transformations
摘要
使用绘图库时,能够变换形状变得非常重要。在本章中,我们看到了 p5.js transform
函数是如何工作的。我们学习了translate
和rotate
函数。我们还学习了angleMode
函数,它让我们设置rotate
函数使用的单位。
然后,我们学习了push
和pop
函数,并了解了如何将它们与转换函数结合使用,以隔离状态并将转换应用于各个形状。虽然这些函数对学习 JavaScript 并不重要,但我发现在使用 p5.js 时了解它们是非常必要的。
实践
在进入下一章之前,尝试自己制作一些很酷的东西,我们将一起制作一个互动游戏!
十三、最终项目
在这一章中,我们将构建一个游戏,它利用了我们到目前为止所看到的一切。在这个过程中,我们还将学习更多的技巧。事实上,我们可以使用 p5.js 库构建一个简单的游戏,这给人留下了非常深刻的印象,也很好地说明了这个库的能力。
我们的游戏会很简单。这是一个打字速度游戏,我们将迅速显示数字给玩家,并希望玩家使用键盘在屏幕上输入当前的数字。如果他们在给定的时间内输入正确的数字,他们就得分了。我们将跟踪分数,以便能够在游戏结束时显示出来。如果游戏能呈现出强烈的视觉体验就太好了,但是主要的焦点还是要围绕着正确的游戏逻辑。
让我们对我们需要创造的事物进行分类:
- 我们需要每隔 N 帧在屏幕上显示一个数字。
- 我们不希望数字在屏幕上保持不变。它应该是动画的,以便随着时间的推移更容易或更难阅读。
- 该号码需要保留在屏幕上,直到显示下一个号码,或者直到玩家按下一个键试图匹配该号码。
- 如果玩家输入与屏幕上的数字匹配,我们将显示一条成功消息。如果不是,将指示失败。
- 我们需要记录成功和失败的次数。在 X 个帧或尝试之后,向用户显示结果。
- 我们需要在游戏结束后找到重启游戏的方法。
入门指南
我们列表中的第一项是能够在屏幕上定期显示一个唯一的数字。还记得我们之前使用了remainder
操作符(%)来实现这个壮举。这里,我们将每隔 100 帧在屏幕上显示一个介于 0 和 9 之间的数字(列表 13-1 )。
var content;
function setup() {
createCanvas(800, 300);
textAlign(CENTER, CENTER);
}
function draw() {
background(220);
if (frameCount === 1 || frameCount % 100 === 0) {
content = parseInt(random(10), 10);
}
text(content, width/2, height/2);
}
Listing 13-1Displaying a random integer
every 100 frames
在这个例子中,我们首先在全局范围内初始化一个名为content
的变量。然后在draw
函数中,我们使用random
函数在第一帧或每 100 帧生成一个随机数,然后将该值保存在content
变量中。然而,random
函数的问题是它返回一个浮点数。为了这个游戏的目的,我们想要整数。所以我们使用parseInt
函数将浮点数转换成整数。记住,parseInt
函数要求您传递第二个参数来设置操作的数字系统的基数,对于常见的用例来说,数字系统几乎总是 10。
我们将生成的数字存储在一个名为content
的变量中,然后将该变量传递给一个text
函数,在屏幕中间显示出来。
我们将需要从我们将在屏幕上显示的数字自定义行为很多;所以我们将创建一个 JavaScript 对象来表示它。这样,我们创建的操作数字的函数(如变换操作、颜色配置等)。)可以保持分组在帮助组织程序的对象下。我们将称这个新对象为GuessItem
。我很清楚这是一个可怕的名字,但正如他们所说,在计算机科学中有两个难题:缓存失效、命名事物和一个接一个的错误。
如果我们在尝试创建一个包装 p5.js text
函数的 JavaScript 对象之后再来看我们的代码,看起来我们似乎毫无理由地增加了额外的复杂性,因为我们的代码几乎增长了两倍。但是在一个对象下包含文本绘制功能将有助于以后组织我们的代码。见清单 13-2 。
var guessItem;
function setup() {
createCanvas(800, 300);
}
function draw() {
if (frameCount === 1 || frameCount % 100 === 0) {
background(220);
guessItem = new GuessItem(width/2, height/2, 1);
}
guessItem.render();
}
function GuessItem(x, y, scl) {
this.x = x;
this.y = y;
this.scale = scl;
this.content = getContent();
function getContent() {
// generate a random integer in between 0 and 9
return parseInt(random(10), 10);
}
this.render = function () {
push();
textAlign(CENTER, CENTER);
translate(this.x, this.y);
scale(this.scale);
text(this.content, 0, 0);
pop();
}
}
Listing 13-2
Text drawing functionality
我们先来关注一下GuessItem
对象。GuessItem
是一个创建对象的构造函数,它需要三个参数:x 和 y 位置以及它在屏幕上绘制的形状的比例。它本身也有两个methods
。其中之一是getContent
,它产生一个介于 0 和 10 之间的随机数,并将其存储在一个名为content
的属性中。它包含的另一个方法是render
,在屏幕上显示一个GuessItem
对象实例的content
属性。
render
方法中的每个操作都存在于push
和pop
函数调用中。这允许我们包含在这个对象包含的这个方法中发生的设置和转换相关的状态变化。这里,我们使用translate
和scale
变换函数来改变文本对象的位置和大小。我们之前没有看到scale
函数,但它是一个与translate
和rotate
函数非常相似的变换函数。顾名思义,它控制的是绘图区域的比例,和其他变换函数的工作原理类似,所以最好将其包含在push
和pop
函数之间。
我们可以使用一个textSize
函数来调用大小,但是我通常发现使用转换函数更直观一些。
在清单 13-3 中,我们现在将使用这个GuessItem
构造函数来创建一个绘制到屏幕上的对象。我们用几个参数on line 10
实例化一个GuessItem
对象,并将它保存在一个名为guessItem
的变量中。
guessItem = new GuessItem(width/2, height/2, 1);
Listing 13-3Creating a GuessItem instance
GuessItem
将要显示的数字也是在实例化时确定的。使用这个对象拥有的render
方法将这个对象绘制到屏幕上on line 13
(清单 13-4 )。
guessItem.render();
Listing 13-4Using the
render method
在清单 13-5 中,让我们让文本在它的生命周期中不断增长,为游戏增加一些活力。
var guessItem;
function setup() {
createCanvas(800, 300);
}
function draw() {
background(220);
if (frameCount === 1 || frameCount % 100 === 0) {
guessItem = new GuessItem(width / 2, height / 2, 1);
}
guessItem.render();
}
function GuessItem(x, y, scl) {
this.x = x;
this.y = y;
this.scale = scl;
this.scaleIncrement = 1;
this.content = getContent();
function getContent() {
// generate a random integer in between 0 and 9
return parseInt(random(10), 10);
}
this.render = function() {
push();
textAlign(CENTER, CENTER);
translate(this.x, this.y);
scale(this.scale);
text(this.content, 0, 0);
// increase the scale value by the increment value with each render
this.scale = this.scale + this.scaleIncrement;
pop();
}
}
Listing 13-5Making the text grow in size
我们添加了一种方法,通过每次调用render
函数来递增scale
函数(清单 13-6 )。
this.scale = this.scale + this.scaleIncrement;
Listing 13-6Increment the scale function
我们还在名为scaleIncrement
的GuessItem
构造函数中添加了一个控制缩放速度的新变量。使用该值可以改变动画的速度。例如,我们可以增加这个值来增加游戏难度。
在清单 13-7 中,我们将在我们的脚本中添加更多的参数化,以便能够控制数字显示的方式和频率。
var guessItem
;
// controls the frequency that a new random number is generated.
var interval = 100;
function setup() {
createCanvas(800, 300);
}
function draw() {
background(220);
if (frameCount === 1 || frameCount % interval === 0) {
guessItem = new GuessItem(width / 2, height / 2, 1);
}
guessItem.render();
}
function GuessItem(x, y, scl) {
this.x = x;
this.y = y;
this.scale = scl;
this.scaleIncrement = 0.5;
this.content = getContent();
this.alpha = 255;
this.alphaDecrement = 3;
function getContent() {
// generate a random integer in between 0 and 9
return parseInt(random(10), 10);
}
this.render = function() {
push();
fill(0, this.alpha);
textAlign(CENTER, CENTER);
translate(this.x, this.y);
scale(this.scale);
text(this.content, 0, 0);
// increase the scale value by the increment value with each render
this.scale = this.scale + this.scaleIncrement;
// decrease the alpha value by the decrement value with each render
this.alpha = this.alpha - this.alphaDecrement;
pop();
}
}
Listing 13-7Controlling the frequency of numbers
在这里,我们有几个更小的调整。我们在render
方法中添加了一个fill
函数,我们现在动态地为显示的数字设置 alpha,以使每一帧更加透明。我认为这增加了游戏的活力。将这个数字设得小一点,看看事情变得有压力。我们还使用一个名为interval
的全局变量来参数化GuessItem
的创建频率,这样我们就可以使用该变量的值来使游戏变得更容易或更难。
顺便问一下,你能猜出我们为什么给数字生成函数取名为getContent
吗?那是因为我们做完这个游戏之后,更新游戏在屏幕上显示文字而不是数字应该是一件相当琐碎的事情。让我们的函数名保持通用有助于我们将来为这个游戏所做的扩展工作。
到目前为止,我们只完成了待办事项列表中的两个项目,即通过使用给定的时间间隔在屏幕上显示一个数字,并在屏幕上动画显示该数字,以给我们的游戏添加活力。在下一节中,我们将处理玩家交互。
用户交互
我们还有一项突出的任务,就是获取用户输入,并将其与屏幕上的数字进行比较。让我们实现它(清单 13-8 )。
var guessItem = null;
// controls the frequency that a new random number is generated.
var interval = 100;
var solution = null;
function setup() {
createCanvas(800, 300);
}
function draw() {
background(220);
if (frameCount === 1 || frameCount % interval === 0) {
solution = null;
guessItem = new GuessItem(width / 2, height / 2, 1);
}
if (guessItem) {
guessItem.render();
}
if (solution === true) {
background(255);
} else if (solution === false) {
background(0);
}
}
function keyPressed() {
if (guessItem !== null) {
// check to see if the pressed key matches to the displayed number.
// if so set the solution global variable to a corresponding value.
console.log('you pressed: ', key);
solution = guessItem.solve(key);
console.log(solution)
guessItem = null;
} else {
console.log('nothing to be solved');
}
}
function GuessItem(x, y, scl) {
this.x = x;
this.y = y;
this.scale = scl;
this.scaleIncrement = 0.5;
this.content = getContent();
this.alpha = 255;
this.alphaDecrement = 3;
this.solved = null;
function getContent() {
// generate a random integer in between 0 and 9
return parseInt(random(10), 10);
}
this.solve = function(input) {
// check to see if the given input is equivalent to the content.
// set solved to the corresponding value.
var solved;
if (input === this.content) {
solved = true;
} else {
solved = false;
}
this.solved = solved;
return solved;
}
this.render = function() {
push();
if (this.solved === false) {
return;
}
fill(0, this.alpha);
textAlign(CENTER, CENTER);
translate(this.x, this.y);
scale(this.scale);
text(this.content, 0, 0);
// increase the scale value by the increment value with each render
this.scale = this.scale + this.scaleIncrement;
// decrease the alpha value by the decrement value with each render
this.alpha = this.alpha - this.alphaDecrement;
pop();
}
}
Listing 13-8
Fetching and comparing user input
我们在很多地方更新了代码。为了能够完成我们的任务,我们在名为solve
的GuessItem
对象上实现了一个新方法。solve
方法获取用户输入,并根据给定的用户输入是否与GuessItem content
变量匹配,返回true
或false
。我们最终将结果保存在一个solution
全局变量中(清单 13-9 )。
this.solve = function(input) {
// check to see if the given input is equivalent to the content.
// set solved to the corresponding value.
var solved;
if (input === this.content) {
solved = true;
} else {
solved = false;
}
this.solved = solved;
return solved;
}
Listing 13-9Solve method inside the GuessItem
为了能够获得用户输入,我们创建了一个 p5.js 事件函数keyPressed
,每当用户按下一个键时都会调用这个函数。在这个keyPressed
函数中,我们调用一个guessItem
对象的solve
方法来查看被按下的键是否与guessItem
的内容相匹配。如果是,则解变量为true
,如果不是,则为false
。
function keyPressed() {
// check to see if the pressed key matches to the displayed number.
// if so set the solution global variable to a corresponding value.
if (guessItem !== null) {
console.log('you pressed: ', key);
solution = guessItem.solve(key);
console.log(solution)
guessItem = null;
} else {
console.log('nothing to be solved');
}
}
Listing 13-10Handling key press
如果有一个GuessItem
存在,我们只从玩家那里读取按键。这是因为一旦玩家做出猜测,我们现在就给guessItem
变量赋予一个null
。这样做可以有效地去除当前的guessItem
对象。这使得我们可以防止玩家对一个数字进行多次猜测。由于guessItem
变量现在可以有一个null
变量,这意味着游戏中可能没有猜测项,因为用户试图猜测它的值,我们对render
方法的调用可能会失败。为了防止这种情况发生,我们将那个render
调用放在一个条件中。此外,我们在keyPressed
函数中有几个console.log
函数,通过查看控制台消息来了解发生了什么。
作为一项测试措施,我们增加了一个条件,如果玩家猜测错误,将背景颜色改为黑色,如果玩家猜测正确,则使用solution
变量将背景颜色改为白色。
说了这么多,这段代码现在不工作。甚至我们正确的猜测都是把屏幕变黑。你能猜到原因吗?
原来原因是keyPressed
函数将被按下的键捕获为字符串,而GuessItem
对象中生成的内容是一个数字。使用三重等号,===
,我们正在寻找这两个值之间是否有严格的相等,没有。这是因为数字永远不等于字符串。所以,我们的函数返回false
。为了解决这个问题,我们将使用 JavaScript 函数String
将生成的数字转换成一个字符串(清单 13-11 )。
function getContent() {
return String(parseInt(random(10), 10));
}
Listing 13-11Converting the random integer
to a string
保持用户分数
为了能够向用户反馈他们在游戏中的表现,我们将开始存储他们的分数。我们将利用这些存储的数据让游戏在一定数量的猜测或失败后停止(清单 13-12 )。
var guessItem = null;
// controls the frequency that a new random number is generated.
var interval = 100;
// an array to store solution values
var results = [];
var solution = null;
function setup() {
createCanvas(800, 300);
}
function draw() {
background(220);
if (frameCount === 1 || frameCount % interval === 0) {
solution = null;
guessItem = new GuessItem(width/2, height/2, 1);
}
if (guessItem) {
guessItem.render();
}
if (solution === true) {
background(255);
} else if (solution === false) {
background(0);
}
}
function keyPressed() {
if (guessItem !== null) {
// check to see if the pressed key matches to the displayed number.
// if so set the solution global variable to a corresponding value.
console.log('you pressed: ', key);
solution = guessItem.solve(key);
console.log(solution);
if (solution) {
results.push(true);
} else {
results.push(false);
}
guessItem = null;
} else {
console.log('nothing to be solved');
}
}
function GuessItem(x, y, scl) {
this.x = x;
this.y = y;
this.scale = scl;
this.scaleIncrement = 0.5;
this.content = getContent();
this.alpha = 255;
this.alphaDecrement = 3;
this.solved = null;
function getContent() {
// generate a random integer in between 0 and 9
return String(parseInt(random(10), 10));
}
this.solve = function(input) {
// check to see if the given input is equivalent to the content.
// set solved to the corresponding value.
var solved;
if (input === this.content) {
solved = true;
} else {
solved = false;
}
this.solved = solved;
return solved;
}
this.render = function () {
push();
if (this.solved === false) {
return;
}
fill(0, this.alpha);
textAlign(CENTER, CENTER);
translate(this.x, this.y);
scale(this.scale);
text(this.content, 0, 0);
// increase the scale value by the increment value with each render
this.scale = this.scale + this.scaleIncrement;
// decrease the alpha value by the decrement value with each render
this.alpha = this.alpha - this.alphaDecrement;
pop();
}
}
Listing 13-12
Storing scores
在清单 13-13 中,我们创建了一个results
数组来存储玩家分数。每当玩家做出一个正确的猜测,我们就在那里推一个true
值;每次玩家猜错了,我们就按一个false
。
if (solution) {
results.push(true);
} else {
results.push(false);
}
Listing 13-13Creating a results array
我们还应该构建一些功能来获取results
array
的值并对其进行评估。为此,我们将构建一个名为getGameScore
的函数(清单 13-14 )。它将获得results
数组,并对其进行评估,以查看当前用户得分。
var guessItem = null;
// controls the frequency that a new random number is generated
var interval = 100;
// an array to store solution values
var results = [];
var solution = null;
function setup() {
createCanvas(800, 300);
}
function draw() {
// if there are 3 losses or 10 attempts stop the game
var gameScore = getGameScore(results);
if (gameScore.loss === 3 || gameScore.total === 10) {
return;
}
background(220);
if (frameCount === 1 || frameCount % interval === 0) {
solution = null;
guessItem = new GuessItem(width/2, height/2, 1);
}
if (guessItem) {
guessItem.render();
}
if (solution === true) {
background(255);
} else if (solution === false) {
background(0);
}
}
function getGameScore(score) {
// given a score array, calculate the number of wins and losses.
var wins = 0;
var losses = 0;
var total = score.length;
for (var i = 0; i < total; i++) {
var item = score[i];
if (item === true) {
wins = wins+1;
} else {
losses = losses+1;
}
}
return {win: wins, loss: losses, total: total};
}
function keyPressed() {
if (guessItem !== null) {
// check to see if the pressed key matches to the displayed number.
// if so set the solution global variable to a corresponding value.
console.log('you pressed: ', key);
solution = guessItem.solve(key);
console.log(solution);
if (solution) {
results.push(true);
} else {
results.push(false);
}
guessItem = null;
} else {
console.log('nothing to be solved');
}
}
function GuessItem(x, y, scl) {
this.x = x;
this.y = y;
this.scale = scl;
this.scaleIncrement = 0.5;
this.content = getContent();
this.alpha = 255;
this.alphaDecrement = 3;
this.solved;
function getContent() {
// generate a random integer in between 0 and 9
return String(parseInt(random(10), 10));
}
this.solve = function(input) {
// check to see if the given input is equivalent to the content.
// set solved to the corresponding value.
var solved;
if (input === this.content) {
solved = true;
} else {
solved = false;
}
this.solved = solved;
return solved;
}
this.render = function () {
push();
if (this.solved === false) {
return;
}
fill(0, this.alpha);
textAlign(CENTER, CENTER);
translate(this.x, this.y);
scale(this.scale);
text(this.content, 0, 0);
// increase the scale value by the increment value with each render
this.scale = this.scale + this.scaleIncrement;
// decrease the alpha value by the decrement value with each render
this.alpha = this.alpha - this.alphaDecrement;
pop();
}
}
Listing 13-14Building a
getGameScore function
我们的脚本越来越大,越来越复杂!在清单 13-15 中是我们最近添加的函数:getGameScore
。它获取score
变量并遍历该变量来合计输赢的次数,以及猜测的总数。
function getGameScore(score) {
var wins = 0;
var losses = 0;
var total = score.length;
for (var i = 0; i < total; i++) {
var item = score[i];
if (item === true) {
wins = wins+1;
} else {
losses = losses+1;
}
}
return {win: wins, loss: losses, total: total};
}
Listing 13-15Calculating the game score using the getGameScore function
我们在draw
函数的开头添加了一个条件来检查getGameScore
函数的结果。如果有 3 次失败或总共 10 次猜测,条件执行基本上有一个return
语句的内容(清单 13-16 )。
var gameScore = getGameScore(results);
if (gameScore.loss === 3 || gameScore.total === 10) {
return;
}
Listing 13-16Conditionally stopping
the game
如清单 13-17 所示,在return
语句之后的任何一行都不会被执行,因为当前的循环将终止,新的循环将开始——只要玩家的分数保持不变,新的循环也将终止。
if (gameScore.loss === 3 || gameScore.total === 10) {
return;
}
Listing 13-17Using the return statement
to stop the draw loop
我们需要一个机制来重启游戏。如清单 13-18 所示,首先,我们将构建一个在游戏结束时显示的屏幕,以显示玩家的分数,并提示玩家按一个键,ENTER,以重新开始游戏(图 13-1 )。其次,我们会让它在游戏结束后,如果玩家按下回车键,它就会重新启动。
图 13-1
Output from Listing 13-18
var guessItem = null;
// controls the frequency that a new random number is generated.
var interval = 100;
// an array to store solution values
var results = [];
var solution = null;
// stores if the game is over or not.
var gameOver = false;
function setup() {
createCanvas(800, 300);
}
function draw() {
var gameScore = getGameScore(results);
if (gameScore.loss === 3 || gameScore.total === 10) {
gameOver = true;
displayGameOver(gameScore);
return;
}
background(220);
if (frameCount === 1 || frameCount % interval === 0) {
solution = null;
guessItem = new GuessItem(width/2, height/2, 1);
}
if (guessItem) {
guessItem.render();
}
if (solution === true) {
background(255);
} else if (solution === false) {
background(0);
}
}
function displayGameOver(score) {
// create the Game Over screen
push();
background(255);
textSize(24);
textAlign(CENTER, CENTER);
translate(width / 2, height / 2);
fill(237, 34, 93);
text('GAME OVER!', 0, 0);
translate(0, 36);
fill(0);
// have spaces inside the strings for the text to look proper.
text('You have ' + score.win + ' correct guesses', 0, 0);
translate(0, 100);
textSize(16);
var alternatingValue = map(sin(frameCount / 10), -1, 1, 0, 255);
fill(237, 34, 93, alternatingValue);
text('PRESS ENTER', 0, 0);
pop();
}
function getGameScore(score) {
// given a score array, calculate the number of wins and losses.
var wins = 0;
var losses = 0;
var total = score.length;
for (var i = 0; i < total; i++) {
var item = score[i];
if (item === true) {
wins = wins+1;
} else {
losses = losses+1;
}
}
return {
win: wins,
loss: losses,
total: total
};
}
function restartTheGame() {
// sets the game state to start.
results = [];
solution = null;
gameOver = false;
}
function keyPressed() {
// if game is over, then restart the game on ENTER key press.
if (gameOver === true) {
if (keyCode === ENTER) {
console.log('restart the game');
restartTheGame();
return;
}
}
if (guessItem !== null) {
// check to see if the pressed key matches to the displayed number.
// if so set the solution global variable to a corresponding value.
console.log('you pressed: ', key);
solution = guessItem.solve(key);
console.log(solution);
if (solution) {
results.push(true);
} else {
results.push(false);
}
guessItem = null;
} else {
console.log('nothing to be solved');
}
}
function GuessItem(x, y, scl) {
this.x = x;
this.y = y;
this.scale = scl;
this.scaleIncrement = 0.5;
this.content = getContent();
this.alpha = 255;
this.alphaDecrement = 3;
this.solved = null;
function getContent() {
return String(parseInt(random(10), 10));
}
this.solve = function(input) {
var solved;
if (input === this.content) {
solved = true;
} else {
solved = false;
}
this.solved = solved;
return solved;
}
this.render = function() {
push();
if (this.solved === false) {
return;
}
fill(0, this.alpha);
textAlign(CENTER, CENTER);
translate(this.x, this.y);
scale(this.scale);
text(this.content, 0, 0);
// increase the scale value by the increment value with each render
this.scale = this.scale + this.scaleIncrement;
// decrease the alpha value by the decrement value with each render
this.alpha = this.alpha - this.alphaDecrement;
pop();
}
}
Listing 13-18Restarting the game
让我们先看看我们用displayGameOver
函数做了什么(清单 13-19 )。这里发生了一些我们以前不知道的事情。
function displayGameOver(score) {
push();
background(255);
textSize(24);
textAlign(CENTER, CENTER);
translate(width/2, height/2);
fill(237, 34, 93);
text('GAME OVER!', 0, 0);
translate(0, 36);
fill(0);
// have spaces inside the strings for the text to look proper.
text('You have ' + score.win + ' correct guesses', 0, 0);
translate(0, 100);
textSize(16);
var alternatingValue = map(sin(frameCount/10), -1, 1, 0, 255);
fill(237, 34, 93, alternatingValue);
text('PRESS ENTER', 0, 0);
pop();
}
Listing 13-19
DisplayGameOver function
您应该注意的第一件事是,translate
函数调用的结果是累积的。如果我们在width/2, height/2
之后执行(0, 100)
的translate
,那么得到的translate
将是width/2, height/2 + 100
。
这段代码中的另一个新东西是 p5.js sin
和map
函数,我们用它们来创建一个闪烁的文本。一个sin
函数计算角度的正弦值。给定顺序值,产生的sine
值将在-1 和 1 之间交替。但是-1 和 1 在我们的用例中作为数值对我们几乎没有用处。如果我们要用这个值来设置一个fill
函数的alpha
,一个在 0 到 255 之间变化的值将会非常有用。这就是map
功能发挥作用的地方(清单 13-20 )。map
函数将给定范围内的给定值(第二个和第三个参数)映射到新的给定范围(第四个和第五个参数)。
var alternatingValue = map(sin(frameCount/10), -1, 1, 0, 255);
Listing 13-20Using the
map function
我们将介于-1 和 1 之间的sin
函数的结果映射到 0 和 255。
我们可以调用这个新函数向玩家显示消息,而不是简单地执行一个return
语句。我们实现的下一件事是在游戏结束后重启游戏。为此,我们需要两样东西。首先,我们需要一种方法来响应ENTER
键。然后,我们需要重新初始化相关的游戏变量,以创建一个新游戏正在开始的印象。
清单 13-21 显示了响应ENTER
键的keyPressed
功能部分。
if (gameOver === true) {
if (keyCode === ENTER) {
console.log('restart the game');
restartTheGame();
return;
}
}
Listing 13-21Responding to the
ENTER key
我们使用keyCode
变量和ENTER
变量来响应ENTER
按键。
restartTheGame
函数的内容很简单(清单 13-22 )。它只是重新初始化全局范围内的几个变量,比如用户分数,让它重新开始工作。
function restartTheGame() {
// sets the game state to start.
results = [];
solution = null;
gameOver = false;
}
Listing 13-22The
restartTheGame function
这就是了!我们可以继续努力,通过调整机制和增强游戏的视觉效果来让游戏体验变得更好。但是我们已经奠定了构成我们游戏骨架的基础,现在可以根据你的具体需求进一步开发。
最终代码
这是最终的代码(列表 13-23 )。我决定对我正在开发的版本做一些更新。我决定显示数字的单词,而不是显示数字。我发现这在视觉上更令人愉悦,从游戏的角度来看也更具挑战性,因为它增加了一点解析你所看到的东西的开销。我还在GuessItem
中添加了一个名为drawEllipse
的新方法,该方法可以在屏幕上绘制椭圆以及文字,使游戏更具视觉吸引力。最后,我稍微调整了一下游戏参数,以使计时正确,并添加了当玩家输入正确或错误的数字时显示的消息。图 13-2 显示了最终游戏代码的屏幕。
var guessItem = null;
// controls the frequency that a new random number is generated.
var interval = 60; // changing this to make the game feel faster.
// an array to store solution values
var results = [];
var solution = null;
// stores if the game is over or not.
var gameOver = false;
function setup() {
createCanvas(800, 300);
}
function draw() {
// if there are 3 losses or 10 attempts stop the game.
var gameScore = getGameScore(results);
if (gameScore.loss === 3 || gameScore.total === 10) {
gameOver = true;
displayGameOver(gameScore);
return;
}
background(0); // black background
if (frameCount === 1 || frameCount % interval === 0) {
solution = null;
guessItem = new GuessItem(width/2, height/2, 1);
}
if (guessItem) {
guessItem.render();
}
if (solution == true || solution === false) {
// displaying a text on screen instead of flat color.
solutionMessage(gameScore.total, solution);
}
}
function solutionMessage(seed, solution) {
// display a random message based on a true of false solution.
var trueMessages = [
'GOOD JOB!',
'DOING GREAT!',
'OMG!',
'SUCH WIN!',
'I APPRECIATE YOU',
'IMPRESSIVE'
];
var falseMessages = [
'OH NO!',
'BETTER LUCK NEXT TIME!',
'PFTTTT',
':('
];
var messages;
push();
textAlign(CENTER, CENTER);
fill(237, 34, 93);
textSize(36);
randomSeed(seed * 10000);
if (solution === true) {
background(255);
messages = trueMessages;
} else if (solution === false) {
background(0);
messages = falseMessages;
}
text(messages[parseInt(random(messages.length), 10)], width / 2, height / 2);
pop();
}
function displayGameOver(score) {
// create the Game Over screen
push();
background(255);
textSize(24);
textAlign(CENTER, CENTER);
translate(width / 2, height / 2);
fill(237, 34, 93);
text('GAME OVER!', 0, 0);
translate(0, 36);
fill(0);
// have spaces inside the string for the text to look proper.
text('You have ' + score.win + ' correct guesses', 0, 0);
translate(0, 100);
textSize(16);
var alternatingValue = map(sin(frameCount / 10), -1, 1, 0, 255);
fill(237, 34, 93, alternatingValue);
text('PRESS ENTER', 0, 0);
pop();
}
function getGameScore(score) {
// given a score array, calculate the number of wins and losses.
var wins = 0;
var losses = 0;
var total = score.length;
for (var i = 0; i < total; i++) {
var item = score[i];
if (item === true) {
wins = wins + 1;
} else {
losses = losses + 1;
}
}
return {
win: wins,
loss: losses,
total: total
};
}
function restartTheGame() {
// sets the game state to start.
results = [];
solution = null;
gameOver = false;
}
function keyPressed() {
// if game is over, then restart the game on ENTER key press.
if (gameOver === true) {
if (keyCode === ENTER) {
console.log('restart the game');
restartTheGame();
return;
}
}
if (guessItem !== null) {
// check to see if the pressed key matches to the displayed number.
// if so set the solution global variable to a corresponding value.
console.log('you pressed: ', key);
solution = guessItem.solve(key);
console.log(solution);
if (solution) {
results.push(true);
} else {
results.push(false);
}
guessItem = null;
} else {
console.log('nothing to be solved');
}
}
function GuessItem(x, y, scl) {
this.x = x;
this.y = y;
this.scale = scl;
this.scaleIncrement = 0.25;
this.clr = 255;
this.content = getContent();
this.alpha = 255;
this.alphaDecrement = 6;
this.solved = null;
this.contentMap = {
'1': 'one',
'2': 'two',
'3': 'three',
'4': 'four',
'5': 'five',
'6': 'six',
'7': 'seven',
'8': 'eight',
'9': 'nine',
'0': 'zero'
};
this.colors = [
[63, 184, 175],
[127, 199, 175],
[218, 216, 167],
[255, 158, 157],
[255, 61, 127],
[55, 191, 211],
[159, 223, 82],
[234, 209, 43],
[250, 69, 8],
[194, 13, 0]
];
function getContent() {
// generate a random integer in between 0 and 9
return String(parseInt(random(10), 10));
}
this.solve = function(input) {
// check to see if the given input is equivalent to the content.
// set solved to the corresponding value.
var solved;
if (input === this.content) {
solved = true;
} else {
solved = false;
}
this.solved = solved;
return solved;
}
this.drawEllipse = function(size, strkWeight, speedMultiplier, seed) {
// draw an animated ellipse with a random color to the screen.
push();
randomSeed(seed);
translate(this.x, this.y);
var ellipseSize = this.scale * speedMultiplier;
scale(ellipseSize);
var clr = this.colors[parseInt(random(this.colors.length), 10)]
stroke(clr);
noFill();
strokeWeight(strkWeight);
ellipse(0, 0, size, size);
pop();
}
this.render = function() {
push();
this.drawEllipse(100, 15, 2, 1 * this.content * 1000);
this.drawEllipse(60, 7, 2, 1 * this.content * 2000);
this.drawEllipse(35, 3, 1.2, 1 * this.content * 3000);
pop();
push();
fill(this.clr, this.alpha);
textAlign(CENTER, CENTER);
translate(this.x, this.y);
scale(this.scale);
// display the word for the corresponding number
text(this.contentMap[this.content], 0, 0);
// increase the scale value by the increment value with each render
this.scale = this.scale + this.scaleIncrement;
// decrease the alpha value by the decrement value with each render
this.alpha = this.alpha - this.alphaDecrement;
pop();
}
}
Listing 13-23The final code
图 13-2
Screen from the final game code
代码最大的变化是solutionMessage
函数,所以让我们更详细地看一下(清单 13-24 )。以前我们只是使用基于solution
变量的值的if-else
语句来决定屏幕上显示什么。如果解决方案是true
,我们显示白色背景,如果解决方案是false
,我们显示黑色背景。
现在,如果解决方案是这些值(true
或false
)中的一个,我们将把它传递给一个名为solutionMessage
的函数,该函数使用gameScore.total
作为random
函数的种子,选择一个随机消息来显示。
if (solution == true || solution === false) {
solutionMessage(gameScore.total, solution);
}
Listing 13-24Displaying a message on the screen
如清单 13-25 所示,在solutionMessage
函数中,有两个数组,它们包含一堆基于solution
的值显示的消息值。
if (solution === true) {
background(255);
messages = trueMessages;
} else if (solution === false) {
background(0);
messages = falseMessages;
}
Listing 13-25Conditionally choosing a message
在清单 13-26 中,我们通过将random
函数的返回值转换成整数,从这些数组中选取一个随机值。
text(messages[parseInt(random(messages.length), 10)], width / 2, height / 2);
Listing 13-26Choosing a random message
摘要
这绝对是一个具有挑战性的例子,它检验了我们目前所学的一切。
令人印象深刻的是,我们只需使用 p5.js 就可以构建一个可以在网络上运行、可供数百万人玩的游戏。这也没那么难。整个程序只有 200 行左右。当然还有改进的空间,我们可以根据玩家的表现使游戏难度动态化,添加更多的视觉天赋,并添加一个动态评分系统,在这个系统中,我们可以根据猜测数字所需的时间为正确的猜测分配不同的分数。游戏可以转换成显示文字而不是数字。它可以显示你需要输入名字的图像或者你需要回答的计算。可能性数不胜数!
话虽如此,如果我们想构建更高级的项目,p5.js 可能不是创建游戏的最佳平台。一个合适的游戏库应该有一些特性,比如资源加载系统,精灵支持,碰撞检测,物理引擎,粒子系统…这些在构建高级游戏时经常需要。不过,这并不是说你不能用 p5.js 来构建游戏。我们刚刚证明了这是完全可能的。只是有其他的库围绕着这个解决方案更加专业,而 p5.js 更适合于在网络上创建交互式的动画体验。但是通过学习 p5.js,你不仅学习了如何使用 JavaScript 和它擅长的所有事情,而且你也发展了对在 JavaScript 生态系统中与其他第三方库合作的理解。
标签:function,函数,p5,手册,var,js,我们 From: https://www.cnblogs.com/apachecn/p/18443297