二、入门指南

正在安装 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 可以执行任何前端代码。

A462229_1_En_2_Fig2_HTML.jpg

图 2-2

p5.js online editor

A462229_1_En_2_Fig1_HTML.jpg

图 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 代码编辑器中启动一个新项目时,我们看到的是两个函数声明,它们的名字分别是:setupdraw(列表 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 执行。令人意想不到的是,setupdraw函数也被执行了。这是意外的,因为这些只是函数声明;它们定义了函数的行为,但是我们仍然需要执行这些函数才能使用它们。

这意味着,如果我们只是使用 JavaScript,我们需要显式调用setupdraw函数,以便显示其中的console.log消息:

setup();
draw();
console.log('hello');

但是我们不需要使用 p5.js 库来实现这一点。由于 p5.js 库是如何构建的,它寻找名为setupdraw的函数声明,并为我们执行这些函数。p5.js 之所以控制这些函数的执行,是因为它以一种非常特定的方式执行它们。

p5.js 只执行一次setup函数,然后继续以重复的方式执行 draw 函数,这样如果我们不停止这个过程,它就会一直工作下去。对于任何图形界面来说,这都是一个非常标准的行为——想想网络浏览器、你玩的游戏或者你使用的操作系统。这些只是持续工作并显示在屏幕上的程序,直到我们明确地关闭它们。这就是为什么 p5.js 为draw函数创建了一个执行循环,这样事情就会持续出现在屏幕上,而不是出现一秒钟然后消失。

关于函数的更多信息

让我们多谈谈函数,因为它们是我们将要编写的程序的组成部分。

函数名通常是动词。它们表示通过执行该功能可以执行的特定操作。假设说,我们可能有一个名为drawCat的函数,当它被调用时,可以在屏幕上画一只猫:

drawCat();

然而,这根本不是假设,因为我实际上为这一章创建了一个名为drawCat的猫绘图函数(图 2-3 )。我们可以自由地用 JavaScript 创建任何我们想创建的函数,这给了我们在编写应用程序时巨大的力量。

A462229_1_En_2_Fig3_HTML.jpg

图 2-3

The graphic output of the drawCat function

好吧,平心而论,这个函数在画一只猫方面做得并不太好。

要使用一个函数,我们通过它的名字来调用它,然后在它后面加上括号来执行这个函数。有时,根据函数的创建或定义方式,函数会被参数化。这意味着它们可以接受会影响函数结果的输入值。例如,drawCat函数可能得到一个数字输入,它将决定所画的猫的大小。或者可能输入的数字决定了屏幕上会吸引多少只猫。这真的取决于这个函数是如何构造的。

在我们的示例中,我创建的这个函数可以获得一个输入,该输入允许我们更改在屏幕上绘制的猫头的大小(图 2-4 ):

A462229_1_En_2_Fig4_HTML.jpg

图 2-4

Drawing a cat face

drawCat(2);

不幸的是,p5.js 没有自带drawCat函数——我不得不自己创建——但是它有很多其他有用的函数,可以让我们以简单的方式执行复杂的任务。为了能够使用 p5.js 库做任何事情,我们将使用它附带的函数,这些函数是由创建这个库的聪明人编写的。

这里有一个来自 p5.js 库中的函数,可能我们将要写的所有草图都需要这个函数:函数createCanvascreateCanvas函数的作用是在网页内部创建一个绘图区域画布,供我们工作。但是为了让这个函数工作,我们需要为它提供两个逗号分隔的值:绘图区域的宽度和高度。我们应该在setup函数中调用createCanvas函数,因为它只需要执行一次,而且需要在我们进行任何绘图之前执行。

让我们为这个函数提供值800300,并执行我们的草图来看看发生了什么(清单 2-10 )。看起来变化不大,但启动的浏览器窗口的大小似乎增加了。它现在使用我们提供的维度。让我们再次更改尺寸,以查看窗口大小的更新。

function setup() {
        createCanvas(800, 300);
}

function draw() {
}

Listing 2-10Working with the createCanvas function

还有一个我们会经常用到的函数,叫做backgroundbackground函数使用给定的值设置画布的颜色。我们将在另一章中研究颜色值是如何在 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 首先为我们创建画布,然后将背景设置为灰色。

值得再次强调的是:setupdraw是 p5.js 正确工作所需的函数定义。当我们使用 p5.js 时,我们的工作是确定在这些由 p5.js 执行的函数中放置了什么。这是由 p5.js 的架构决定的。p5.js 的创建者希望确保我们将编写的一些代码只执行一次,用于初始化和设置目的,而一些代码将一直执行,用于绘图、动画和交互目的。

我们在这些函数定义中使用了 p5.js 库中的函数,比如createCanvasbackground。这些函数已经被其他人定义了,所以我们实际上不知道它们里面包含了什么代码。但是我们并不真的需要这些知识,因为我们关心的只是它们做什么以及如何使用它们。

函数允许我们以简单的方式执行复杂的任务。通过使用createCanvas函数,我们不需要知道在页面中创建 canvas 元素需要做哪些工作。这些细节对我们来说是隐藏的、抽象的。我们只需要知道如何调用这个函数,让它为我们工作。

最后,我们将再调用一个函数,这次是在draw函数定义中,在页面上绘制一个矩形(清单 2-12 )。

为了画一个矩形,我们将利用一个名为rect的函数。rect函数要求我们为它提供四个输入值:画布绘制区域内矩形左上角的 x 和 y 位置,以及矩形的宽度和高度值。

在不知道 p5.js 中坐标是如何工作的情况下,我们将只提供这个函数 x 值为 50,y 值为 100,宽度为 200,高度为 100(图 2-5 )。

A462229_1_En_2_Fig5_HTML.jpg

图 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 )。

A462229_1_En_2_Fig6_HTML.jpg

图 2-6

Coordinate origins

当我们在屏幕上绘制一个矩形时,提供的坐标定义了矩形的左上角(列表 2-13 和图 2-7 )。

A462229_1_En_2_Fig7_HTML.jpg

图 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函数定义下。

A462229_1_En_2_Fig8_HTML.jpg

图 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 函数,它都会将自己绘制在前面的形状之上。我们可以改变函数调用的顺序来影响形状的堆叠顺序。

A462229_1_En_2_Fig9_HTML.jpg

图 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,并在屏幕上画出形状。

我们已经看到,我们需要在两个名为setupdraw的函数定义块中编写代码。任何只需要执行一次的东西都被放在setup函数下,任何我们想要制作动画或与之交互的东西都被放入draw函数中。将我们的代码写入这两个函数是 p5.js 要求我们做的事情。它不是一般的编程原则、惯例或类似的东西。我们可以使用不同的库,它不需要对我们的代码进行这种结构化。这个需求与 p5.js 作为一个库的架构有关。我们需要从这两个函数定义开始我们所有的 p5.js 草图。

像这样需要重复编写的代码被称为boilerplate代码。有很多样板文件从来都不是一件好事,因为我们会发现自己不得不重复我们的工作很多,但在这种情况下,样板文件的数量是非常容易管理的。

在这些函数定义中,我们使用了 p5.js 库中的函数,比如createCanvasbackground,和一些形状函数,比如rect。如前所述,函数是通用的编程结构,它允许我们将代码捆绑在一起以实现可重用性。函数也从我们身上抽象出大量的复杂性。我们不需要知道一个函数是如何工作的;我们只需要知道如何使用它。我们完全不知道createCanvas实际上是如何在网页中创建画布元素的。只要我们知道如何使用这个功能就没关系。想到开车;我们不一定需要知道内燃机如何工作才能驱动它。我们只需要知道如何使用方向盘、踏板等与汽车互动。类似的想法也适用于函数。

稍后,我们将创建我们的函数来管理程序的复杂性,并创建可重用的代码。

实践

尝试重新创建图 2-10 中的图像。

A462229_1_En_2_Fig10_HTML.jpg

图 2-10

Practice image