安卓 Processing 教程(一)
一、Android 模式入门
在本章中,我们将介绍处理软件和 Android 模式,它们背后的社区项目,以及我们如何开始使用该模式为 Android 设备创建应用。
加工项目是什么?
Processing project 是一个社区项目,致力于分享知识、促进教育和促进基于代码的艺术和设计的多样性。处理软件是这一举措的核心部分,现在由处理基金会( https://processingfoundation.org/ )指导。该处理软件由麻省理工学院媒体实验室的 Casey Reas 和 Ben Fry 于 2001 年创建,作为计算艺术和设计的教学和制作工具,并从那时起一直在不断发展。可在 https://processing.org/ 下载,其源代码在自由软件许可(GPL 和 LGPL)下发布。从现在开始,我在谈加工软件的时候,就简单的指加工。
处理包括两个互补的部分:语言和开发环境。它们共同组成了一个“软件草图”,旨在允许用代码快速表达视觉想法,同时还提供足够的空间让这些想法发展成为成熟的项目。处理已经被用来在生成艺术、数据可视化和交互式装置中创建许多美丽和鼓舞人心的作品,其中一些被包括在处理网站( https://processing.org/exhibition/ )上的策划列表中。
Processing 语言
Processing 语言包括一组用于处理屏幕绘图、数据输入/输出和用户交互的功能。处理项目( https://processing.org/people/ )背后的一个小型志愿者团队精心构建了这组功能,技术上称为应用接口或 API,通过简单一致的命名约定、明确的行为和明确定义的范围来简化图形和交互式应用的开发。虽然最初是在 Java 中实现的,但处理 API 目前可以在许多编程语言中使用,包括 Python、JavaScript 和 r。然而,正是这个 API 的 Java 实现,以及对 Java 语言的一些简化,定义了 Processing 语言。尽管有这种区别,但在整本书中,我将互换使用术语 Processing 语言和 API,因为在 Android 的上下文中,我们本质上将使用处理 API 的 Java 实现。
在 2001 年以来的积极发展中,Processing 语言现在不仅包含函数之间的大约 300 项,还包含类和常量( https://processing.org/reference/ )。这种语言的一个定义性特征是,它提供了用很少的代码创建一个能够显示交互式图形的程序的可能性。正如我所提到的,它还包含了许多关于 Java 语言的简化,目的是让不熟悉计算机代码的人更容易学习。下面的程序举例说明了 Processing 语言的这些特性:
color bg = 150;
void setup() {
size(200, 200);
}
void draw() {
background(bg);
ellipse(mouseX, mouseY, 100, 100);
}
这个程序的输出是一个 200×200 像素的窗口,其中包含一个跟随鼠标移动的白色圆圈;该窗口的背景是灰色的。函数setup()和draw()几乎出现在任何加工程序中,并驱动其“绘图循环”程序的所有初始化都应该在setup()中进行,在程序启动时只执行一次。然后,包含所有绘图指令的draw()函数每秒被连续调用几次(默认情况下,60 次),这样程序的图形输出就可以随时间而变化。
但是,如果您熟悉 Java,您可能已经注意到这段代码不是有效的 Java 程序。例如,没有封装所有代码的主类的显式定义,也没有 Java 中初始化处理显示和用户输入的“窗口工具包”所需的附加指令。这个程序需要在处理开发环境中运行,处理开发环境对处理代码应用“预处理”步骤,以便将其转换成有效的 Java 程序。然而,这种转换发生在幕后,处理用户根本不需要担心。
处理开发环境
处理开发环境(PDE)是一个应用,它为我们提供了一个简化的代码编辑器来编写、调试和运行处理程序,称为草图(图 1-1 )。PDE 还整合了一个整洁的用户界面,以处理用它创建的所有草图,并添加库和其他扩展 PDE 核心功能的外部组件,如 p5.js、Python 或 Android 模式。
图 1-1。
The Processing development environment showing a running sketch in Java mode
PDE 和 Processing 语言的简单易用是这本“代码速写本”的关键要素对于许多想开始从事代码工作的人来说,一个绊脚石是现代开发环境的复杂性,比如 Eclipse 或 IntelliJ,就冗长的安装和压倒性的用户界面而言。相比之下,PDE 通过提供简单的安装过程和最小化的界面来解决这些问题,而加工草图的简单结构使用户能够快速获得视觉反馈。Processing 的目标是支持类似于用笔和纸画草图的迭代开发过程,在这个过程中,人们可以从一个简单的想法开始,并通过连续的草图来完善它。
Note
处理 API 可以在 PDE 之外使用;例如,在更高级的集成开发环境(IDE)中,如 Eclipse、NetBeans 或 IntelliJ。当使用这些 ide 编写程序时,Processing 的所有绘图、数据和交互 API 都是可用的;然而,Processing 语言对于 Java 的简化将会丢失。
我们可以从主网站( https://processing.org/download )下载最新版本的处理。正如上一段所指出的,安装相当简单,只需要打开。zip(在 Windows 和 Mac 上)或。包含 PDE 和所有其他核心文件的 tgz(在 Linux 上)包。然后,我们应该能够从Home或Applications文件夹中的任何位置运行 PDE,而不需要任何额外的步骤。
PDE 在 sketchbook 文件夹中组织用户草图。每个草图都存储在 sketchbook 内的子文件夹中,而 sketchbook 又包含一个或多个带有。pde 扩展。默认情况下,处理会在用户帐户中的Documents文件夹内创建 sketchbook 文件夹(例如,Mac 上的/Users/andres/Documents/Processing),但是可以通过在首选项窗口中选择所需的 sketchbook 文件夹来更改该位置,该窗口位于 Mac 上的处理菜单下以及 Windows 和 Linux 上的文件菜单下(图 1-2 )。请注意顶部的素描本位置。
图 1-2。
The Preferences window on Mac
延伸加工
正如我在开始提到的,处理项目不仅是 PDE 或语言,而且,非常重要的是,围绕软件的使用和共享、教学和包容性的目标建立的社区。由于 Processing 的开放性和模块化架构,许多人对“核心”软件做出了改进和扩展。这些贡献属于以下四类之一:
- 库:模块(包含一个或多个构建在 jar 包中的 Java 代码文件,以及附加的文档和示例文件),使得在草图中访问新的功能成为可能。例如,用于计算机视觉的 OpenCV 库(仅适用于 PC/Mac),或用于 Android 传感器的 Ketai(在第 7 和 8 章节中介绍)。
- 编程模式:可选的代码编辑器和相关的 PDE 定制,允许在 PDE 中使用完全不同的语言。例如安卓模式。我们将在本章的下一节看到如何安装 Android 模式。
- 工具:只能从处理中运行的应用,提供特定的功能来帮助编写代码、调试和测试草图。例如拾色器(在第二章中讨论)。
- 示例:可以用作学习材质或参考的贡献代码草图包。如丹尼尔·希夫曼(
http://learningprocessing.com/)的《学习处理》一书中的草图。
通过贡献的库、模式、工具和示例进行处理的扩展,使其能够扩展到不属于原始软件的应用领域,如移动应用、计算机视觉和物理计算,同时保持核心功能的简单性和对新程序员的可访问性。
投稿经理
默认情况下,处理包括一种默认模式,Java,在这种模式下,我们可以使用 Processing 语言的 Java 实现在 Windows、Mac 和 Linux 计算机上编写和运行草图。Processing 还捆绑了几个“核心”库,其中一些是 OpenGL(用于绘制硬件加速的 2D 和 3D 场景)、pdf(将图形导出为 pdf 文件)和 data(允许处理 CSV 和 JSON 等格式的数据文件)。
为了安装额外的贡献,我们可以使用贡献管理器(CM ),它使得这个过程无缝。CM 的截图如图 1-3 所示。CM 有五个选项卡,前四个用于每种贡献类型——库、模式、工具和示例——第五个用于更新。作者在中央存储库中注册的所有贡献都可以通过 CM 访问,并且在有新版本可用时也可以通过 CM 更新。
图 1-3。
The Contribution Manager in Processing, showing the Modes tab Note
作者没有注册的贡献,因此不能通过 CM 使用,仍然可以手动安装。我们需要下载包含库、模式、工具或示例的包,通常是 zip 格式,并将其解压缩到 sketchbook 文件夹中。库、模式、工具和示例都有单独的子文件夹。更多信息见 https://processing.org/reference/libraries/ 。
Android 处理
与处理软件本身一样,Android 的处理也有几个方面。首先,它是一个始于 2009 年的社区工作,旨在支持使用 Processing 开发 Android 应用,并将该项目的一些概念转化为移动应用的上下文:迭代草图、简单性和可访问性。
从软件的角度来看,Processing for Android 由 processing-android 库和自定义 PDE 编程模式本身组成。该库是包含处理 API 的所有功能的包,但针对 Android 平台进行了重新实现。Android mode 提供了 PDE 的定制版本,允许我们编写处理代码,并在 Android 设备或仿真器上运行。Android mode 包括 processing-android 库,我们需要它来运行我们的处理代码而不出错。然而,这些区别在这一点上并不重要,因为处理将让我们安装和使用 Android 模式,而不必担心处理-android 库。对于那些打算在更高级的应用中使用 Android 处理的人来说,这个库将变得更加重要。
Note
处理-android 库可以从像 android Studio 这样的 IDE 中导入,允许使用常规 Android 应用中的所有处理功能。附录 a 中介绍了这种高级用法。
安装 Android 模式
一旦我们在计算机上安装了 Processing,我们应该能够通过运行 Processing 应用打开 PDE,然后我们可以通过 CM 安装最新版本的 Android mode。该模式还需要 Android 软件开发工具包(SDK)才能工作。Android SDK 是 Google 为开发和调试 Android 应用而提供的一组库、工具、文档和其他支持文件。因此,要安装 Android 模式和 SDK(如果需要的话),请遵循以下步骤:
-
If a valid SDK is detected on the computer, Processing will ask if we want to use it or download a new one (Figure 1-5). Because the SDK is very large (up to several GBs), it can be a good idea to use the one that is already installed to save disk space. However, if that SDK is also used by another development tool, such as Android Studio, it may get updated outside Processing, which may lead to incompatibilities with the mode.
图 1-5。
Choosing between using an existing SDK or downloading a new one automatically (top), and between locating an SDK manually or downloading one automatically (bottom)
-
如果没有检测到有效的 Android SDK,处理将要求手动定位 SDK 或自动下载 SDK(图 1-5 )。
-
Open the CM by clicking the “Add Mode…” option that appears in the drop-down menu in the upper-right corner of the PDE (Figure 1-4).
图 1-4。
Opening the Contribution Manager to add a new mode
-
在模式选项卡中选择 Android 模式条目,然后单击安装按钮。
-
安装完成后,关闭 CM,使用图 1-4 中相同的下拉菜单切换到 Android 模式。
Note
安卓模式 4.0 版本需要安卓 8.0 版本(奥利奥),对应 API 级( https://source.android.com/source/build-numbers )。该模式的自动 SDK 下载将从谷歌服务器检索这个版本。
Android 模式的预发布版本以及旧的、不受支持的版本不再可以通过 CM 获得,而是存放在 GitHub 发布页面( https://github.com/processing/processing-android/releases )上,可以通过下载相应的文件并将其解压缩到 Processing 的 sketchbook 中的Modes文件夹中来手动安装。
Android 模式的界面
Android 模式下的编辑器和 Java 模式下的编辑器非常相似。工具栏包含播放和停止按钮,用于启动草图和停止草图的执行(在设备上或模拟器中)。编辑器中的代码自动完成功能也是可用的。但是,Android mode 的 4.0 版本没有提供集成的调试器。主菜单还包含许多 Android 特有的选项(图 1-6 )。“文件”菜单中有选项可以将当前草图导出为一个可上传到谷歌 Play 商店的包,或者导出为一个可以用 Android Studio 打开的项目。“草图”菜单包含在设备上或模拟器中运行草图的独立选项,以及包含多个选项的独立 Android 菜单,其中包括草图的目标输出类型(常规应用、壁纸、watch face 或 VR 应用)以及当前连接到计算机的 Android 设备列表。所有这些选项将在后续章节中介绍。
图 1-6。
Android-specific options in the interface of Android mode
在设备上运行草图
一旦我们用 PDE 编写了一些草图代码,我们就可以在 Android 手机、平板电脑或手表上运行它。我们需要首先确保为我们的设备打开“USB 调试”。这样做的过程因设备和设备上安装的 Android 操作系统版本而异。在大多数情况下,此设置位于“系统设置”下的“开发人员选项”中。在 Android 4.2 和更高版本中,默认情况下开发者选项是隐藏的,但是我们可以按照以下说明来启用它们:
- 打开设置应用。
- 滚动到底部,选择“关于手机”
- 滚动到底部并轻按“构件号”七次。
- 返回上一个屏幕,在底部找到开发者选项。
打开 USB 调试后(我们只需要做一次),我们必须通过 USB 端口将设备连接到计算机。然后,处理将尝试识别它,并将其添加到 Android 菜单中的可用设备列表中。
Note
安卓模式 4.0 版本只支持运行安卓 4.2(果冻豆,API 级)或更新版本的设备。
让我们使用清单 1-1 中的代码作为我们对 Android sketch 的第一次处理!理解其中的每一行代码并不重要,因为我们将在接下来的章节中详细介绍处理 API。这段代码只是在接受触摸按压的那一半屏幕上画了一个黑色方块。
void setup() {
fill(0);
}
void draw() {
background(204);
if (mousePressed) {
if (mouseX < width/2) rect(0, 0, width/2, height);
else rect(width/2, 0, width/2, height);
}
}
Listing 1-1.Our First Processing for Android Sketch
可以将多个设备同时连接到计算机,但在“设备”菜单中只能选择一个作为“活动”设备,我们的草图将在这里安装和运行。图 1-7 显示了我们已经加载到 PDE 中的第一个草图,以及运行它所选择的设备。
图 1-7。
Selecting the device to run the sketch on
在我们选择了活动设备后,我们可以点击运行按钮或选择草图菜单下的“在设备上运行”。我们应该看到一些消息向下滚动到 PDE 的控制台,同时处理编译草图,将其打包为调试应用,并将其安装到设备上。一个重要的细节是,第一次运行草图时,计算机需要连接到互联网。Processing 使用一个名为 Gradle 的工具从草图的源代码中构建应用。Android mode 自带“Gradle wrapper”,所以我们不需要手动安装 Gradle,但是 wrapper 会在第一次调用时自动下载 Gradle 工具的其余部分。我们可以在第一次运行草图后离线。如果一切顺利,草图应该启动并显示在设备的屏幕上,如图 1-8 所示。
图 1-8。
Running a sketch on a connected phone Note
如果我们运行的是 Windows,就需要安装一个专门的 USB 驱动来连接设备( https://developer.android.com/studio/run/oem-usb.html )。如果我们在处理过程中自动下载了 Android SDK,那么 Nexus 设备的最新谷歌 USB 驱动程序将会在sketchbook文件夹内的android\sdk子文件夹下;如C:\Users\andres\Documents\Processing\android\sdk\extras\google\usb_driver。
如果我们运行的是 Linux,我们可能需要安装一些额外的包( https://developer.android.com/studio/run/device.html )。此外,确保 USB 连接没有配置为“仅充电”
在模拟器中运行草图
如果我们没有运行草图的设备,我们可以使用模拟器。仿真器是一种创建物理设备的软件副本的程序。这个复制品被称为 Android 虚拟设备(AVD),尽管它通常比真实设备慢,但它可以用于在我们目前没有的硬件上测试草图。
我们第一次在仿真器中运行草图时,处理将下载包含在我们的计算机上创建 AVD 所需的所有信息的系统映像(图 1-9 )。但是,它最初会询问我们是要使用“ARM”还是“x86”映像。之所以会这样,是因为安卓设备用的是 ARM CPUs,而台式电脑用的是 x86 处理器。使用带有 ARM 映像的 AVD 时,仿真器会将 ARM 指令逐个转换为 x86 指令,速度较慢。但是如果我们使用 x86 映像,我们计算机中的 CPU 将能够更直接、更快速地模拟 AVD 的 CPU。使用 x86 映像的一个缺点是,我们必须在 Mac 或 Windows 上安装名为 HAXM 的附加软件。由于下载的 HAXM 和 SDK 一起处理,它会为我们安装它,以防我们决定使用 x86 映像。
图 1-9。
System image download dialog in Android mode
我们还必须记住,HAXM 只与 Intel 处理器兼容,所以如果我们的计算机有 AMD CPU,模拟器就不能处理 x86 映像。Linux 有自己的 AVD 加速系统,不需要 HAXM,所以我们可以在配有 AMD CPU 的 Linux 电脑上使用 x86 镜像。不过我们需要执行一些额外的配置步骤,这里描述了: https://developer.android.com/studio/run/emulator-acceleration.html#vm-linux 。
下载完成后,可能需要几分钟,这取决于互联网连接(仿真器的系统映像大小约为 900 MB),处理将启动仿真器,然后在其中启动草图。一旦我们的清单 1-1 在模拟器中运行,它应该如图 1-10 所示。
图 1-10。
Running our sketch in the emulator
摘要
在第一章中,我们已经了解了什么是处理项目和软件,以及我们如何通过 Android 编程模式使用处理来创建应用。正如我们所看到的,处理软件的一些主要特征是它的最小界面和代码项目的简单结构,它被称为草图。这些特性让我们可以非常快速地在设备或仿真器上开始编写和测试自己的草图。
二、Processing 语言
如果您不熟悉 Processing 语言,请阅读本章,了解 2D 形状的创建、几何变换和颜色的使用,以及如何处理触摸屏输入。本章以一个绘制草图的分步示例结束,我们将在第三章中使用它来学习如何将通过处理创建的应用导出和上传到谷歌 Play 商店。
艺术家和设计师的编程速写本
正如我们在前一章中所学的,Processing 语言,结合 PDE,使编程新手更容易开始创建交互式图形。这种语言被设计成最小化、简单易学,但又有足够的表现力来创建不同领域的基于代码的项目:生成艺术、数据可视化、声音艺术、电影、表演等等。它包括大约 200 个不同类别的函数——绘图、交互、排版等等——以及几个帮助处理表单、颜色和数据的类。
我们也可以将处理视为“编码素描本”,类似于我们用来快速起草和提炼想法的纸质素描本。这个类比的一个重要部分是,与纸质速写本一样,处理使我们能够尽快从代码中获得视觉反馈。下一节将描述处理过程中的基本结构,它使我们能够轻松地在屏幕上生成动画输出。
加工草图的设置/绘制结构
在大多数情况下,我们需要我们的处理草图连续运行,以便在屏幕上显示动画图形并跟踪用户输入。我们可以使用一个基本的代码结构来实现这样的交互式草图,其中我们首先在一个setup()函数中执行所有的初始化操作,然后在每次处理需要渲染一个新帧时运行一个draw()函数。
Note
本章中的所有代码示例都可以在 Java 或 Android 模式下运行,因为它们不依赖于特定于任何一种模式的 Processing 语言的任何特性。此外,因为这本书的一个要求是对编程语言有一定程度的了解,所以我们不会讨论编程的基本概念(例如,条件、循环、注释)。
有了这个结构,我们可以创建一个每秒更新固定次数的动画,默认情况下是 60 次。在对draw()函数的每次调用中,我们不仅需要绘制构成合成的可视元素,还需要执行更新合成所需的所有计算。例如,在清单 2-1 中,我们使用函数line(x, 0, x, height)绘制了一条从左向右水平穿过屏幕的垂直线。线的水平位置包含在变量x中,我们用x = (x + 1) % width在每一帧中更新它。在这一行代码中,我们将x加 1,然后计算屏幕宽度模的结果。由于“a对b取模”定义为a除以b的整数的余数(例如9 % 4是1),所以结果只能是 0 到 b - 1 之间的数。因此,我们草图中的x不能小于 0,也不能大于 width - 1,这正是我们所需要的:x一次递增 1 个单位,到达右边缘后折回 0。该草图的输出如图 2-1 所示。
图 2-1。
Output of the animated line sketch
int x = 0;
void setup() {
size(600, 200);
strokeWeight(2);
stroke(255);
}
void draw() {
background(50);
line(x, 0, x, height);
x = (x + 1) % width;
}
Listing 2-1.A Sketch That Draws a Vertical Line Moving Horizontally Across the Screen
处理以每秒 60 帧的默认帧率调用draw()函数;然而,我们可以使用函数frameRate(int fps)来改变这个默认值。例如,如果我们在setup(),中添加frameRate(1),草图将每秒绘制 1 帧。
有时,我们可能需要在几帧之后停止处理动画。我们可以分别使用noLoop()和loop()函数来停止和恢复动画。处理过程中有一个名为looping的布尔(逻辑)变量,根据草图是否运行动画循环,该变量为真或为假。我们可以在前面的代码中添加简单的击键检测功能来停止/恢复草图,这在清单 2-2 中实现。
int x = 0;
void setup() {
size(600, 200);
strokeWeight(2);
stroke(255);
}
void draw() {
background(50);
line(x, 0, x, height);
x = (x + 1) % width;
}
void keyPressed() {
if (looping) {
noLoop();
} else {
loop();
}
}
Listing 2-2.Pausing/Resuming the Animation Loop
除了这些交互式草图,我们还可以创建没有设置/绘制的静态草图,如果我们只想生成不需要更新的固定组合,这通常很有用。处理只运行这些草图中的代码一次。清单 2-3 包含一个简单的静态草图,画出了图 2-2 中看到的白色圆圈。
图 2-2。
Output of the static sketch
size(400, 400);
ellipse(200, 200, 150, 150);
Listing 2-3.Static Sketch Without setup() and draw() Functions
用代码绘图
上一节中的例子指出了基于代码的绘制中的一些重要概念。首先,我们需要指定想要在屏幕上绘制的元素的坐标;二是有函数,比如line(),可以让我们通过设置定义形状的适当数值,来绘制各种图元或形状;第三,我们可以设置这些形状的视觉“样式”(例如,笔画颜色和粗细)。
在处理过程中,我们可以绘制不同种类的形状(点、线、多边形)和特定的属性(笔画粗细和颜色、填充颜色等)。这些属性可以被认为是“样式参数”,一旦设置,就会影响之后绘制的所有内容。例如,清单 2-4 中的每个圆圈有不同的填充颜色,但是如果我们注释掉第二个fill()调用,第一个和第二个圆圈将是红色的,因为在开始时设置的填充颜色会影响前两个ellipse调用。图 2-3 显示了该草图在这些情况下的输出。
图 2-3。
Effect of the calling order of style functions
size(460, 200);
strokeWeight(5);
fill(255, 0, 0);
stroke(0, 255, 0);
ellipse(100, 100, 200, 200);
fill(255, 0, 200); // Comment this line out to make second circle red
stroke(0);
ellipse(250, 100, 100, 100);
fill(0, 200, 200);
ellipse(380, 100, 160, 160);
Listing 2-4.Setting Style Attributes
屏幕坐标
处理将其图形输出绘制到一个矩形像素网格中,沿水平方向(x 轴)从 0 到 width–1,沿垂直方向(y 轴)从 0 到 height–1,如图 2-4 所示。当在 Java 模式下运行代码时,该网格将包含在一个单独的输出窗口中,或者当使用 Android 模式时,该网格将位于设备屏幕的中心。
图 2-4。
Diagram of the screen’s pixels
在用 Processing 绘图时,我们需要记住 x 坐标是从左到右的,而 y 坐标是从上到下的。因此,像素(0,0)表示屏幕的左上角,像素(width-1,height-1)表示右下角。大多数 2D 绘图函数在处理过程中的参数指的是屏幕的像素坐标。例如,下面的示例代码将产生如图 2-5 所示的输出(为清晰起见,其中每个方块代表一个像素)。
图 2-5。
Pixels covered by a stroked rectangle in Processing
stroke(200, 0, 0);
fill(100, 200, 100);
rect(2, 1, width – 1, height - 2);
我们应该根据屏幕大小的限制来调整我们用 Processing 绘制的形状的大小。一般情况下,参考屏幕尺寸时,建议使用width和height内部变量,而不是实际值;这样,我们可以重新调整尺寸,而不必修改绘图代码,就像清单 2-5 中所做的那样。
Size(800, 800);
stroke(0);
fill(180);
background(97);
line(width/2, 0, width/2, height);
line(0, height/2, width, height/2);
rect(0, 0, 200, 200);
rect(width – 200, 0, 199, 200);
rect(width – 200, height – 200, 199, 199);
rect(0, height – 200, 199, 199);
rect(200, 200, width – 400, height – 400);
Listing 2-5.Using screen coordinates.
在这段代码中,一些矩形的宽度/高度不寻常,为 199。这就是为什么屏幕外边框上的线条是可见的,因为正如我们刚刚看到的,最后一行/列像素的 x 坐标是 height-1/width-1。草图的输出,所有外部笔画都落在边缘像素上,如图 2-6 所示,就像它出现在 Nexus 5X 手机上一样。您还会注意到,这个输出只占据了屏幕中心 800 × 800 的正方形,因为这是我们在代码中指定的大小。我们将在本章后面看到如何使用整个屏幕,在第四章我们将看到如何根据设备的分辨率缩放图形。
图 2-6。
Output of code Listing 2-5, on a Nexus 5X phone
形式
我们可以通过处理生成的所有视觉形式都被绘制成二维或三维形状。通常,我们通过在beginShape()和endShape()函数中显式指定定义其边界的所有顶点来构造这些形状,如清单 2-6 所示(其输出如图 2-7 所示)。
图 2-7。
Composition created with several shapes
size(600, 300);
beginShape(QUADS);
vertex(5, 250);
vertex(590, 250);
vertex(590, 290);
vertex(5, 290);
endShape();
beginShape();
vertex(30, 25);
vertex(90, 90);
vertex(210, 10);
vertex(160, 120);
vertex(210, 270);
vertex(110, 180);
vertex(10, 270);
vertex(60, 150);
endShape(CLOSE);
beginShape(TRIANGLES);
vertex(50, 30);
vertex(90, 75);
vertex(110, 30);
endShape();
ellipse(470, 80, 70, 70);
Listing 2-6.Using beginShape() and endShape()
尽管我们在第一个例子中没有使用beginShape / endShape,在第一个例子中,我们用内置函数ellipse()和rect()创建了原始形状,但这些只是对它们对应的beginShape / endShape调用的速记调用。事实上,我们可以使用beginShape(int kind)创建其他类型的图元形状,其中kind参数表示所需的图元。例如,在清单 2-7 中,我们用一组三角形构造了一个正多边形,这些三角形从一个中心顶点呈扇形散开。
size(300, 300);
int numTriangles = 10;
beginShape(TRIANGLE_FAN);
vertex(width/2, height/2);
for (int i = 0; i <= numTriangles; i++) {
float a = map(i, 0, numTriangles, 0, TWO_PI);
float x = width/2 + 100 * cos(a);
float y = height/2 + 100 * sin(a);
vertex(x, y);
}
endShape();
Listing 2-7.Creating a Triangle Fan
在这个例子中,我们使用一个for循环来迭代三角扇形的划分数。Processing 作为 Java 语言的扩展,继承了 Java 的所有控制结构,这是我们算法绘图所需要的。另外,注意函数map()的使用,它是处理 API 的一部分。这个函数非常有用,它允许我们将一个范围内的数值转换为另一个范围内的相应值。在这种情况下,索引i在 0 和numTriangles之间变化,我们要将其转换为 0 和 2π之间的角度。
图 2-8。
Outputs of the triangle fan example for different numbers of vertices
其他种类的原始形状有TRIANGLE_STRIP、QUAD_STRIP、LINES和POINTS,这些都在 Processing 的参考资料中有完整的记录。例如,当你需要创建一个矩形网格或一个挖空的圆时,QUAD_STRIP就变得很方便,就像我们在清单 2-8 中所做的那样。
size(300, 300);
beginShape(QUAD_STRIP);
int numQuads = 10;
for (int i = 0; i <= numQuads; i++) {
float a = map(i, 0, numQuads, 0, TWO_PI);
float x0 = width/2 + 100 * cos(a);
float y0 = height/2 + 100 * sin(a);
float x1 = width/2 + 130 * cos(a);
float y1 = height/2 + 130 * sin(a);
vertex(x0, y0);
vertex(x1, y1);
}
endShape();
Listing 2-8.Creating a Quad Strip
通过调整numQuads变量的值,我们可以获得更多细节的几何图形,如图 2-9 所示。
图 2-9。
Quad strip example with different values for numQuads
然而,我们经常需要创建更复杂的形状,如曲线。尽管我们可以手动计算曲线上的顶点,但 Processing 提供了许多函数来精确地完成这项工作,特别是对于 Catmull-Rom 样条以及二次和三次贝塞尔曲线。例如,bezierVertex()函数允许我们在三次贝塞尔曲线上定义一个点。它需要曲线必须通过的锚点以及定义开始和结束方向的控制点。当开始一条贝塞尔曲线时,第一个锚点用一个常规的vertex()调用设置,如图 2-10 所示。
图 2-10。
Parameters of the bezierVertex() function
我们可以将几条贝塞尔曲线组合成一个形状,以便生成更复杂的图形,如清单 2-9 所示。
size(300, 300);
int numLobes = 4;
float radAnchor = 50;
float radControl = 150;
float centerX = width/2;
float centerY = height/2;
beginShape();
for (int i = 0; i < numLobes; i++) {
float a = map(i, 0, numLobes, 0, TWO_PI);
float a1 = map(i + 1, 0, numLobes, 0, TWO_PI);
float cx0 = centerX + radControl * cos(a);
float cy0 = centerY + radControl * sin(a);
float cx1 = centerX + radControl * cos(a1);
float cy1 = centerY + radControl * sin(a1);
float x0 = centerX + radAnchor * cos(a);
float y0 = centerY + radAnchor * sin(a);
float x1 = centerX + radAnchor * cos(a1);
float y1 = centerY + radAnchor * sin(a1);
vertex(x0, y0);
bezierVertex(cx0, cy0, cx1, cy1, x1, y1);
}
endShape();
Listing 2-9.Creating Multi-lobed Shape with Bezier Curves
通过调整草图中的参数(叶瓣数、锚点半径、控制点半径),我们可以获得一整个系列的形状,其中一些我们可以在图 2-11 中看到。
图 2-11。
Family of multi-lobed shapes created with Bezier curves
颜色
颜色是视觉设计的另一个重要组成部分,除了整个输出屏幕的背景颜色之外,Processing 还提供了许多功能来设置形状内部的颜色(填充颜色)和边缘的颜色(描边颜色)。
默认情况下,我们可以使用 0 到 255 之间的 RGB(红、绿、蓝)值来设置颜色,如清单 2-10 中的代码及其在图 2-12 中的输出所示。
图 2-12。
Output of setting stroke and fill RGB colors
size(600, 300);
strokeWeight(5);
fill(214, 87, 58);
stroke(53, 124, 115);
rect(10, 10, 180, 280);
stroke(115, 48, 128);
fill(252, 215, 51);
rect(210, 10, 180, 280);
stroke(224, 155, 73);
fill(17, 76, 131);
rect(410, 10, 180, 280);
Listing 2-10.Setting Fill and Stroke Colors Using RGB Values
即使我们可以使用 RGB 值创建几乎任何可以想象的颜色,也很难找到我们需要的颜色的正确数字组合。处理包括一个方便的颜色选择器工具来帮助我们交互地选择颜色,然后我们可以将它作为 RGB 值复制到我们的草图中。在 PDE 的 tools 菜单下,颜色选择器以及任何其他安装的工具都是可用的(图 2-13 )。
图 2-13。
Color Selector tool
我们还可以在 HSB(色调、饱和度和亮度)空间中指定颜色。HSB 模式可以用colorMode()功能设置,这也允许我们设置每个组件的范围。在代码清单 2-11 中,我们通过将圆周围的位置映射到色调来绘制色轮。
size(300, 300);
colorMode(HSB, TWO_PI, 1, 1);
float centerX = width/2;
float centerY = height/2;
float maxRad = width/2;
strokeWeight(2);
stroke(0, 0, 1);
for (int i = 0; i < 6; i++) {
float r0 = map(i, 0, 6, 0, 1);
float r1 = map(i + 1, 0, 6, 0, 1);
beginShape(QUADS);
for (int j = 0; j <= 10; j++) {
float a0 = map(j, 0, 10, 0, TWO_PI);
float a1 = map(j + 1, 0, 10, 0, TWO_PI);
float x0 = centerX + maxRad * r0 * cos(a0);
float y0 = centerY + maxRad * r0 * sin(a0);
float x1 = centerX + maxRad * r1 * cos(a0);
float y1 = centerY + maxRad * r1 * sin(a0);
float x2 = centerX + maxRad * r1 * cos(a1);
float y2 = centerY + maxRad * r1 * sin(a1);
float x3 = centerX + maxRad * r0 * cos(a1);
float y3 = centerY + maxRad * r0 * sin(a1);
fill(a0, r0, 1);
vertex(x0, y0);
vertex(x1, y1);
vertex(x2, y2);
vertex(x3, y3);
}
endShape();
}
Listing 2-11.Drawing a Color Wheel Using HSB Values
让我们在这个例子中注意一些重要的事情。首先,我们将色调的范围设置为 2π,以便使指数和颜色之间的转换更加直接。第二,我们用QUADS代替QUAD_STRIP。我们不能为一个条带中的每个四边形设置单独的颜色,因为它们都与前一个和下一个四边形共享一个公共边。相反,在QUADS形状中,每个四边形都是独立于其他四边形定义的,因此可以有不同的样式属性。我们最终的色轮如图 2-14 所示。
图 2-14。
Output of HSB color wheel example Note
我们还可以用十六进制(hex)格式指定颜色,这在 web 开发中非常常见;即fill(#FF0000)或stroke(#FFFFFF)。
几何变换
到目前为止,我们已经看到了如何构建形状和选择颜色。此外,我们需要能够通过应用平移、旋转和缩放变换来移动它们并改变它们的大小(图 2-15 )。
图 2-15。
The three types of geometric transformations
虽然平移、旋转和缩放的概念很直观,但是很难预测几次连续变换的效果。在考虑变换时,想象一下变换只在应用后影响坐标会有所帮助。例如,如果我们沿 x 轴平移 20 个单位,沿 y 轴平移 30 个单位,那么随后将围绕点(20,30)进行旋转。相反,如果首先应用旋转,那么轴将被旋转,并且将沿着旋转的轴发生平移。因此,如果我们在这个变换链的末端绘制一个形状,它的最终位置可能会有所不同,这取决于它们的完成顺序(图 2-16 )。
图 2-16。
Geometric transformations cannot be exchanged
我们可以用pushMatrix()函数保存当前的转换“状态”,用相应的popMatrix()函数恢复。我们必须总是成对地使用这两个函数。它们允许我们通过只对形状的特定子集设置变换来创建复杂的相对运动。例如,清单 2-12 生成一个动画,显示一个椭圆和一个正方形围绕位于屏幕中心的一个较大的正方形旋转,而较小的正方形也围绕自己的中心旋转。图 2-17 显示了该动画的快照。
图 2-17。
Using pushMatrix() and popMatrix() to keep transformations separate
float angle;
void setup() {
size(400, 400);
rectMode(CENTER);
noStroke();
}
void draw() {
background(170);
translate(width/2, height/2);
rotate(angle);
rect(0, 0, 100, 100);
pushMatrix();
translate(150, 0);
rotate(2 * angle);
scale(0.5);
rect(0, 0, 100, 100);
popMatrix();
translate(0, 180);
ellipse(0, 0, 30, 30);
angle += 0.01;
}
Listing 2-12.Using pushMatrix() and popMatrix()
响应用户输入
键盘输入和触摸屏输入允许我们向草图中输入信息以控制其行为。由于用户可以在任何时候触摸屏幕或按键,而不一定是在处理绘制帧时,因此我们需要一种方法来检索这些信息,无论我们在草图中处于哪个绘制阶段。
Processing 提供了几个内置变量和函数来处理用户输入。变量mouseX和mouseY给出了在 Java 模式下鼠标的当前位置。这些变量在 Android 模式下仍然可用,尽管移动设备通常没有鼠标。在这种情况下,它们只是代表屏幕上第一个触摸点的位置(处理也支持多点触摸交互,这将在第五章中介绍)。mouseX和mouseY都由mousePressed补充,表示鼠标/触摸屏是否被按下。使用这些变量,我们可以用很少的代码创建一个简单的绘图草图,如清单 2-13 中所示。它在手机上的输出将如图 2-18 所示。由于通过size()功能设置的宽度和高度小于屏幕分辨率,我们看到输出区域被我们无法绘制的浅色背景包围。然而,如果我们不使用size(width, height)初始化草图,而是使用fullScreen()功能,我们可以使用整个屏幕。这也有隐藏屏幕顶部的状态栏和底部的导航栏的优点。
图 2-18。
Drawing with ellipses
void setup() {
size(1000, 500);
noStroke();
fill(255, 100);
}
void draw() {
if (mousePressed) {
ellipse(mouseX, mouseY, 50, 50);
}
}
Listing 2-13.A Free-hand Drawing Sketch Using Circles
当mouseX/Y存储鼠标/触摸的当前位置时,处理还提供变量pmouseX和pmouseY,它们存储先前的位置。通过连接pmouseX/Y坐标和mouseX/Y坐标,我们可以画出跟随指针移动的连续线条。清单 2-14 展示了这种技术,它也使用了fullScreen()以便我们可以在整个屏幕表面上绘制,如图 2-19 所示。
图 2-19。
Output of our simple drawing sketch, in full-screen mode
void setup() {
fullScreen();
}
void draw() {
if (mousePressed) {
line(pmouseX, pmouseY, mouseX, mouseY);
}
}
Listing 2-14.Another Free-hand Drawing
Sketch
创建一个画藤蔓的应用
我们最后一部分的目标是编写一个绘图应用,将算法形状融入手绘线条中。一种可能性是用类似于生长的植物、藤蔓、叶子和花的形状来增加由线提供的支架。我们之前学过的贝塞尔曲线可以用来生成看起来很自然的形状。用笔和纸画一些草图(图 2-20 )也可以帮助我们尝试一些视觉创意。
图 2-20。
Sketches for the vine-drawing app
我们可以在之前的草图基础上继续。我们之前忽略的一件事是在形式和颜色上某种程度的“随机性”。处理中的random(float a, float b)函数允许我们在a和b之间选择随机数,然后我们可以在通过bezierVertex()函数构建的叶子/花朵形状中使用这些随机数。在清单 2-15 中,我们应用random函数来引入我们形状的颜色和波瓣数量的变化,图 2-21 显示了三次单独运行的草图输出。
图 2-21。
Output of the flower/leaf sketch
void setup() {
size(600, 200);
frameRate(1);
}
void draw() {
background(180);
drawFlower(100, 100);
drawFlower(300, 100);
drawFlower(500, 100);
}
void drawFlower(float posx, float posy) {
pushMatrix();
translate(posx, posy);
fill(random(255), random(255), random(255), 200);
beginShape();
int n = int(random(4, 10));
for (int i = 0; i < n; i++) {
float a = map(i, 0, n, 0, TWO_PI);
float a1 = map(i + 1, 0, n, 0, TWO_PI);
float r = random(10, 100);
float x = r * cos(a);
float y = r * sin(a);
float x1 = r * cos(a1);
float y1 = r * sin(a1);
vertex(0, 0);
bezierVertex(x, y, x1, y1, 0, 0);
}
endShape();
popMatrix();
}
Listing 2-15.Generating Randomized Flowers/Leaves with Bezier Curves
除了花/叶,我们可以添加一些额外的元素;例如,一个生长中的螺旋分支结束于一个果实。螺旋有一个x(t) = r(t) cos(a(t))、y(t) = r(t) sin(a(t))形式的参数公式(、https://www.khanacademy.org/tag/parametric-equations、),其中参数t从 0 到 1,控制曲线的增长。在调整了径向函数之后,我用r(t) = 1/t达到了令人满意的增长行为,所以我们可以从清单 2-16 中的代码开始绘制一个单螺旋(图 2-22 )。
图 2-22。
Output of our spiral parametric equation sketch
size(300, 300);
noFill();
translate(width/2, height/2);
beginShape();
float maxt = 10;
float maxr = 150;
for (float t = 1; t < maxt; t += 0.1) {
float r = maxr/t;
float x = r * cos(t);
float y = r * sin(t);
vertex(x, y);
}
endShape();
Listing 2-16.Drawing a Spiral Using Parametric Equations
圈数由参数t的最大值控制,而最大半径决定螺旋向外延伸多少。这两个参数将在random()函数的帮助下给我们一些视觉上的变化,就像我们之前做的那样。一个问题是,我们需要螺旋的主干与画线的方向一致。我们可以通过旋转角度加 180 度(π)来沿所需角度定向螺旋。这就是我们在清单 2-17 中所做的,它生成的三个不同的螺旋如图 2-23 所示。
图 2-23。
Output of the randomized spirals sketch .
void setup() {
size(600, 200);
frameRate(1);
}
void draw() {
background(180);
drawSpiral(100, 100, 0);
drawSpiral(300, 100, QUARTER_PI);
drawSpiral(500, 100, PI);
}
void drawSpiral(float posx, float posy, float angle) {
pushMatrix();
translate(posx, posy);
rotate(angle + PI);
noFill();
beginShape();
float maxt = random(5, 20);
float maxr = random(50, 80);
float x0 = maxr * cos(1);
float y0 = maxr * sin(1);
for (float t = 1; t < maxt; t += 0.1) {
float r = maxr/t;
float x = r * cos(t) - x0;
float y = r * sin(t) - y0;
vertex(x, y);
}
endShape();
popMatrix();
}
Listing 2-17.Adding Random Variability to Our Spiral-Generation Algorithm
我们现在可以将所有这些元素放在一个简单的绘图应用中,将树叶、藤蔓和水果添加到手绘线条中(清单 2-18 )。当按下鼠标/触摸屏时,会随机添加树叶和藤蔓。我们可以通过设置在每一帧中绘制新叶子或藤蔓的概率来控制细节的数量。通过使用值 0.05,平均来说,我们将每 20 帧添加一个有按压事件的新元素。通过从当前和先前鼠标/触摸位置之间的差异构建一个PVector对象来计算螺旋藤蔓与最后一条线段的连接角度。PVector是一个处理 2D 和 3D 向量的内置类。这个类包含几个实用函数,其中一个给我们矢量的航向角;即向量沿 x 轴的角度。图 2-24 显示了用该 app 绘制的图纸。
图 2-24。
Output of the vine-drawing sketch
void setup() {
fullScreen();
noFill();
colorMode(HSB, 360, 99, 99);
strokeWeight(2);
stroke(210);
background(0, 0, 99);
}
void draw() {
if (mousePressed) {
line(pmouseX, pmouseY, mouseX, mouseY);
if (random(1) < 0.05) {
PVector dir = new PVector(mouseX - pmouseX, mouseY - pmouseY);
float a = dir.heading();
drawSpiral(mouseX, mouseY, a);
}
if (random(1) < 0.05) {
drawFlower(mouseX, mouseY);
}
}
}
void keyPressed() {
background(0, 0, 99);
}
void drawFlower(float xc, float yc) {
pushMatrix();
pushStyle();
noStroke();
translate(xc, yc);
fill(random(60, 79), random(50, 60), 85, 190);
beginShape();
int numLobes = int(random(4, 10));
for (int i = 0; i <= numLobes; i++) {
float a = map(i, 0, numLobes, 0, TWO_PI);
float a1 = map(i + 1, 0, numLobes, 0, TWO_PI);
float r = random(10, 50);
float x = r * cos(a);
float y = r * sin(a);
float x1 = r * cos(a1);
float y1 = r * sin(a1);
vertex(0, 0);
vertex(0, 0);
bezierVertex(x, y, x1, y1, 0, 0);
}
endShape();
popStyle();
popMatrix();
}
void drawSpiral(float xc, float yc, float a) {
pushMatrix();
pushStyle();
translate(xc, yc);
rotate(PI + a);
noFill();
beginShape();
float maxt = random(5, 10);
float maxr = random(20, 70);
float sign = (random(1) < 0.5) ? -1 : +1;
float x0 = maxr * cos(sign);
float y0 = maxr * sin(sign);
for (float t = 1; t < maxt; t += 0.5) {
float r = maxr/t;
float x = r * cos(sign * t) - x0;
float y = r * sin(sign * t) - y0;
vertex(x, y);
}
endShape();
noStroke();
fill(random(310, 360), 80, 80);
float x1 = (maxr/maxt) * cos(sign * maxt) - x0;
float y1 = (maxr/maxt) * sin(sign * maxt) - y0;
float r = random(5, 10);
ellipse(x1, y1, r, r);
popStyle();
popMatrix();
}
Listing 2-18.Full Vine-Drawing Sketch
摘要
我们现在已经对 Processing 语言有了一个总体的了解,并且能够使用它的一些函数和变量来绘制形状、设置颜色、应用变换以及通过鼠标或触摸屏处理用户交互。尽管我们只涉及了处理中所有可用功能的一小部分,但我们在这里看到的应该给我们足够的材质来探索算法绘图,制作我们自己的交互式草图,并作为 Android 应用运行它们。
三、从草图到游戏商店
在这一章中,我们将回顾创建完整的 Android 项目处理过程中涉及的步骤,从草图绘制、调试到将项目导出为已签名的应用,以便上传到谷歌 Play 商店。我们将使用上一章的草图作为例子。
草图绘制和调试
在前面的章节中,我们强调了“代码草图”的重要性,其中即时的视觉输出和快速迭代是处理项目开发的核心要素。另一个至关重要的组成部分是识别和解决代码中的错误或“bug ”,这个过程称为调试。
调试需要的时间和编写代码本身一样多。使调试具有挑战性的是,一些错误是错误的逻辑或不正确的计算的结果,并且因为代码中没有打字错误或任何其他语法错误,所以处理能够运行草图。不幸的是,没有简单的技术可以消除程序中的所有错误,但是 Processing 提供了一些实用工具来帮助我们。
从控制台获取信息
调试程序最简单的方法是在程序执行流程的不同点打印变量值和消息。Processing 的 API 包括文本打印函数print()和println(),它们输出到 PDE 中的控制台区域。这两个函数的唯一区别是println()在末尾增加了一个新的换行符,而print()没有。清单 3-1 展示了一个草图,它使用println()来指示一个事件的发生(在本例中是鼠标按压)和一个内置变量的值。
void setup() {
fullScreen();
}
void draw() {
println("frame #", frameCount);
}
void mousePressed() {
println("Press event");
}
Listing 3-1.Using println() in a Sketch to Show Information on the Console
加工控制台显示这些功能打印的任何内容,以及指示草图执行过程中出现问题的任何警告或错误信息(图 3-1 )。
图 3-1。
PDE’s console outlined with red
将消息打印到控制台进行调试的主要问题是,它需要为我们想要跟踪的每个变量添加这些额外的函数调用。一旦我们完成了调试,我们需要删除或注释掉所有这些调用,这对于大型草图来说会变得很麻烦。
Note
处理过程中的注释工作方式与 Java 中的完全相同:我们可以使用两个连续的正斜杠、//注释掉一行代码,以及一整块文本,其中/*在块的开头,*/在块的结尾。我们还可以使用 PDE 中编辑菜单下的“注释/取消注释”选项。
使用 logcat 获取更多信息
我们可以从处理控制台获得许多有用的信息,但有时这不足以找出我们的草图有什么问题。Android SDK 包括几个命令行工具,可以帮助我们进行调试。最重要的 SDK 工具是 adb (Android Debug Bridge),它使我们用于开发的计算机和设备或仿真器之间的通信成为可能。事实上,在从 PDE 运行草图时,处理使用 adb 来查询哪些设备可用,并将草图推送到设备或仿真器。
我们也可以手动使用 adb 例如,获得更详细的调试消息。为此,我们需要打开一个终端控制台,进入后,我们必须切换到 Android SDK 的安装目录。如果 SDK 是通过处理自动安装的,那么它应该位于android子文件夹中的 sketchbook 文件夹中。在该文件夹中,SDK 工具位于sdk/platform-tools中。在那里,我们可以使用logcat选项运行 adb 工具,打印出包含所有消息的日志。例如,图 3-2 显示了我们在 Mac 上运行logcat所需的命令序列。
图 3-2。
Terminal session on Mac displaying the commands to run logcat
默认情况下,logcat打印 Android 设备或模拟器生成的所有消息——不仅仅是来自我们正在调试的草图的消息,还有来自所有其他当前正在运行的进程的消息——所以我们可能会得到太多的消息。如果将logcat与–I选项一起使用,可显示加工的打印信息。Logcat 有额外的选项,只显示错误消息(-E)或警告(-W)。完整的选项列表可以在谷歌的开发者网站上找到( https://developer.android.com/studio/command-line/logcat.html )。
使用集成调试器
Java mode in Processing 3.0 引入了一个集成的调试器,使我们更容易跟踪正在运行的草图的内部状态。即使调试器在 Android 模式下不可用,我们仍然可以使用它来调试 Android 草图。如果一个处理草图不依赖于 Android 特定的功能,它应该兼容 Android 和 Java 模式,因为这两种模式的代码 API(几乎)是相同的。在这种情况下,我们可以暂时切换到 Java 模式,利用它的调试器,然后回到 Android 模式,继续在设备或仿真器上工作。
我们打开调试器的方法是,按下模式选择器旁边菜单栏左侧带有蝴蝶图标的按钮,或者在 Debug 菜单中选择“Enable Debugger”。启用后,我们可以访问 PDE 中的几个附加选项,以便在草图运行时使用。例如,我们可以在草图的代码中的任何一行添加检查点。检查点指示草图的执行应该在哪里停止,以允许我们检查草图中所有变量的值,包括用户定义的和内置的变量。
我们可以通过双击代码编辑器左边的行号来创建一个新的检查点。菱形标志将表示该线已用检查点标记。当我们运行一个包含一个或多个检查点的草图时,处理将在到达每个检查点时停止执行,此时我们可以使用变量检查器窗口检查变量的值(图 3-3 )。我们通过按工具栏上的继续按钮来恢复执行。我们也可以通过按下 step 按钮来一行一行地查看每个变量在每行之后如何改变它的值。
图 3-3。
Debugging session with the integrated debugger in Java mode
集成调试器中的所有这些功能可以帮助我们在不添加打印指令的情况下识别代码中的错误,尽管修复棘手的错误总是具有挑战性,即使使用调试器也可能需要很长时间。最后,根据我们从调试器或打印指令中获得的信息,它归结为理解草图中代码的逻辑及其可能的结果和边缘情况。这样,我们可以缩小包含 bug 的代码部分。
报告处理错误
有时,处理草图中的意外或错误行为可能不是草图本身的错误,而是处理核心中的错误。如果你非常怀疑你发现了一个处理 bug,你可以在项目的 GitHub 页面上报告。如果是影响 Android 模式的 bug,请在 https://github.com/processing/processing-android/issues 的处理-android 存储库中打开一个新问题,并尽可能多地包含重现该 bug 的信息,帮助开发人员检查该问题并最终修复它。
准备发布草图
在 PDE 中调试了一个草图之后,我们可能想要打包它,通过谷歌 Play 商店公开发布。当从 PDE 工作时,处理创建一个调试应用包,它只能安装在我们自己的设备上用于测试目的。创建一个适合普遍发行的应用需要一些额外的步骤和考虑,以确保它可以上传到 Play Store。
调整设备的 DPI
为了准备公开发布我们的草图,我们必须首先确保它可以在(大多数)正在使用的 Android 设备上运行。在编写和调试草图时,我们经常使用一个或几个不同的设备,因此很难预测我们无法访问的硬件上的问题。一个常见的情况是,在不同的设备上运行处理草图时,图形看起来要么太大,要么太小。手机、平板电脑和手表的分辨率(像素数量)和物理屏幕尺寸可能会有很大差异,因此,在特定尺寸的屏幕上观看时,以一种分辨率设计的图形元素在另一种设备上可能会看起来不正确。由于 Android 设计为支持各种屏幕尺寸和分辨率的组合,我们需要一种处理方法来适应我们草图的视觉设计,以便它在不同设备上看起来像预期的那样。
分辨率与屏幕尺寸的比率就是所谓的 DPI(每英寸点数,在计算机屏幕的上下文中相当于每英寸像素,或 PPI)。DPI 是比较不同设备的基本数值。请务必记住,较高的 DPI 并不一定意味着较高的分辨率,因为具有相同分辨率的两台不同设备可能具有不同的屏幕尺寸。例如,Galaxy Nexus(对角线 4.65 英寸)的分辨率为 720 × 1280 像素,而 Nexus 7(对角线 7 英寸)的分辨率为 800 × 1280 像素。这些设备的 dpi 分别为 316 和 216,即使 Galaxy Nexus 的分辨率实际上略低于 Nexus 7。
Android 根据以下六个广义密度将设备分类到“密度桶”中(具体设备将根据哪个最接近其实际 DPI 而归入其中一个类别):
- ldpi(低)120 dpi
- mdpi(中等)∼160 dpi
- hdpi(高)240 dpi
- xhdpi(超高)320 dpi
- xxhdpi(超高)480 dpi
- xxxhdpi(超高)640 dpi
正如我们将在本章后面看到的,在生成应用图标时,广义密度水平在处理过程中很重要,但在编写代码时就不那么重要了。为了确保我们草图中的视觉元素能够跨不同设备适当缩放,还有一个来自 Android 的参数,Processing 通过它的 API 提供了这个参数。这是显示密度,一个代表与参考 160 dpi 屏幕(例如,320 × 480,3.5 英寸屏幕)相比,我们设备中的像素大(或小)多少的数字。因此,在 160 dpi 的屏幕上,这个密度值将是 1;在 120 dpi 的屏幕上,它将是 0.75,等等。
Note
Google 关于多屏支持的 API 指南给出了 Android 上密度独立的详细信息: https://developer.android.com/guide/practices/screens_support.html 。
显示密度作为名为displayDensity的常量在处理中可用,我们可以在代码中的任何地方使用它。调整设备 DPI 输出的最简单方法是将草图中所有图形元素的大小乘以displayDensity,,这是清单 3-2 中所示的方法。正如我们在图 3-4 中看到的,草图绘制的圆圈大小在具有不同 dpi 的设备中是相同的。同样,这个例子使用fullScreen()来初始化我们的草图输出到整个屏幕的大小,不管它的分辨率如何。
图 3-4。
From left to right: output of our sketch on a Samsung Galaxy Tab 4 (7", 1280 × 800 px, 216 dpi), Nexus 5X (5.2", 1920 × 800 px, 424 dpi), and a Moto E (4.3", 960 × 540 px, 256 dpi)
void setup() {
fullScreen();
noStroke();
}
void draw() {
background(0);
float r = 50 * displayDensity;
int maxi = int(width/r);
int maxj = int(height/r);
for (int i = 0; i <= maxi; i++) {
float x = map(i, 0, maxi, 0, width);
for (int j = 0; j <= maxj; j++) {
float y = map(j, 0, maxj, 0, height);
ellipse(x, y, r, r);
}
}
}
Listing 3-2.Using displayDensity to Adjust Our Sketch to Different Screen Sizes and Resolutions
我们现在可以回到上一章的藤蔓绘制草图,在需要缩放图形的代码部分添加displayDensity。更具体地说,任何代表形状大小或屏幕上顶点位置的变量或值都应该乘以displayDensity。清单 3-3 显示了应用于原始草图的这些变化。
void drawFlower(float xc, float yc) {
pushMatrix();
pushStyle();
noStroke();
translate(xc, yc);
fill(random(60, 79), random(50, 60), 85, 190);
beginShape();
int numLobes = int(random(4, 10));
for (int i = 0; i <= numLobes; i++) {
float a = map(i, 0, numLobes, 0, TWO_PI);
float a1 = map(i + 1, 0, numLobes, 0, TWO_PI);
float r = random(10, 50) * displayDensity;
...
}
void drawSpiral(float xc, float yc, float a) {
pushMatrix();
pushStyle();
translate(xc, yc);
rotate(PI + a);
noFill();
beginShape();
float maxr = random(20, 70) * displayDensity;
...
fill(random(310, 360), 80, 80);
float x1 = (maxr/maxt) * cos(sign * maxt) - x0;
float y1 = (maxr/maxt) * sin(sign * maxt) - y0;
float r = random(5, 10) * displayDensity;
ellipse(x1, y1, r, r);
popStyle();
popMatrix();
}
Listing 3-3.Adding displayDensity to the Vine-drawing Sketch from Chapter 2
使用模拟器
我们在第一章简要讨论了模拟器。即使我们有自己的设备,模拟器也是有用的,因为它允许我们测试我们无法访问的硬件配置。处理过程会创建一个默认的 Android 虚拟设备(AVD)来在模拟器中运行,但它的分辨率只有 480 × 800 像素,以确保在不同的计算机上有合理的性能。我们可以使用命令行工具avdmanager创建其他具有不同属性的 avd,它包含在 Android SDK 中。我们必须记住,模拟器的运行速度可能会比实际设备慢,尤其是如果您使用的是高分辨率 avd 或具有其他高端功能的 avd。
由于avdmanager是一个命令行工具,我们首先需要打开一个终端控制台并切换到工具目录,其中avdmanager和模拟器启动器位于 SDK 文件夹中。图 3-5 显示了使用 Nexus 5X 手机的设备定义创建新的 AVD,然后使用仿真器启动它的步骤顺序。
图 3-5。
Creating and launching a new AVD from the command line using the avdmanager and emulator tools
在运行avdmanager命令的行中,我们提供了四个参数:
- AVD 的名称,可以是我们希望使用的任何名称
-k "system-images;android-26;google_apis;x86":用于 AVD 的 SDK 包;为了找出 SDK 中可用的 SDK 包,我们需要查看 SDK 文件夹中的 system-images 子文件夹。-d: "Nexus 5X":包含我们想要仿真的设备的硬件参数的设备定义。我们可以通过运行命令'./avdmanager list devices'列出所有可用的设备定义。-p ∼/Documents/Processing/android/avd/n5x:我们将存储该 AVD 的文件夹;在这种情况下,我们使用 sketchbook 文件夹中的android/avd/n5x,因为这是 Android 模式用于默认 AVDs 的默认位置。
图 3-5 中的下一行实际上启动了模拟器,但是在此之前,我们需要设置 AVD 的“皮肤”,告诉模拟器它应该呈现手机屏幕的实际尺寸。目前,avdmanager没有设置设备皮肤的选项,但我们可以手动将其添加到 AVD 的配置文件中,在本例中,该文件位于∼/Documents/Processing/android/avd/n5x中,名为config.ini。我们可以用任何文本编辑器打开这个文件,然后在末尾添加一行skin.name=widthxheight,使用设备的宽度和高度,尽管我们也可以使用我们偏好的其他值,如图 3-6 所示。
图 3-6。
Adding a skin resolution to the AVD’s config.ini file
一旦我们将皮肤分辨率添加到 AVD 的config.ini文件中,我们就可以运行前面显示的仿真器行,它包括以下参数:
-avd n5x:我们要启动的 AVD 的名称-gpu auto:使仿真器能够使用计算机上的硬件加速来更快地渲染 AVD 的屏幕(如果可用的话)。否则,它将使用较慢的软件渲染器。-port 5566:设置连接控制台和 adb 与仿真器的 TCP 端口号。
要使用我们的新 AVD 来代替 Processing 的默认 AVD,我们应该手动启动它,就像我们在本例中所做的那样,然后 Processing 会在其中安装我们的草图,而不是在默认 AVD 中。但是,我们需要确保使用正确的端口参数,因为处理将只能与运行在端口 5566 上的电话模拟器和端口 5576 上的手表模拟器通信。
Note
谷歌的 Android 开发者网站包括 avdmanager ( https://developer.android.com/studio/command-line/avdmanager.html )和从命令行运行仿真器( https://developer.android.com/studio/run/emulator-commandline.html )的页面。在那里,我们可以找到关于这些工具的更多信息。
设置图标和包名
Android 应用要求在应用启动器菜单中以不同的像素密度显示各种大小的图标。从 PDE 运行草图时,处理使用一组默认的通用图标,但这些图标不应用于公开发布。
为了将我们自己的图标添加到项目中,我们需要在中创建以下文件:图标-36、图标-48、图标-72、图标-96、图标-144 和图标-192。ldpi (36 × 36)、mdpi (48 × 48)、hdpi (72 × 27)、xhdpi (96 × 96)、xxhdpi (144 × 144)和 xxxhdpi (192 × 192)分辨率的 PNG 格式。一旦我们有了这些文件,在导出签名包之前,我们把它们放在草图的文件夹中。
对于上一章的藤蔓绘制应用,我们将使用如图 3-7 所示的一组图标。
图 3-7。
Set of icons for the vine-drawing app
谷歌发布了一套遵循公司材质 UI 风格的图标创建指南和资源,可在 https://www.google.com/design/spec/style/icons.html 获得
设置包名和版本
Google App Store 中的应用由一个包名唯一标识,包名是一串类似于com.example.helloworld的文本。这个包名称遵循 Java 包命名惯例,其中应用名称(helloworld)在最后,以相反的顺序(com.example)在开发应用的公司或个人的网站之前。
处理通过在草图名称前添加processing.test来自动构建这个包名。在我们第一次从 PDE(在设备上或者在仿真器中)运行之后,我们可以通过编辑处理在 sketch 文件夹中生成的manifest.xml文件来改变默认的包名。我们还可以设置版本代码和版本名称。例如,在下面处理生成的清单文件中,包名为com.example.vines_draw,版本号为 10,版本名为 0.5.4:
<?xml version="1.0" encoding="UTF-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
android:versionCode="10" android:versionName="0.5.4"
package="com.example.vines_draw">
<uses-sdk android:minSdkVersion="17" android:targetSdkVersion="25"/>
<application android:icon="@drawable/icon"
android:label="Vines Draw">
<activity android:name=".MainActivity"
android:theme=
"@style/Theme.AppCompat.Light.NoActionBar.FullScreen">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
请注意,我们的应用的包名必须是唯一的,因为谷歌 Play 商店上不能有两个应用具有相同的包名。此外,我们应该使用应用标签中的android:label属性来设置应用名称。Android 将使用这个标签作为应用在启动器和 UI 其他部分的可见标题。
作为签名包导出
Android 模式通过签名和对齐应用简化了我们草图的发布,因此我们可以非常轻松地将其上传到 Google Play 开发人员控制台。签名过程包括创建一个包含公钥/私钥对的公钥的公钥证书,这样当应用包被签名时,它会嵌入一个唯一的指纹,该指纹将包与其作者相关联。这确保了该应用的任何未来更新都是真实的,并且来自原作者()https://developer。安卓。com/ studio/ publish/ appsigning。html 。需要对齐来优化封装内的数据存储,从而减少运行应用时消耗的 RAM 量。虽然 Processing 将为我们进行签名和对齐,但我们仍然需要创建一个 Google Play 开发者帐户来使用 Play 控制台,这需要在撰写本文时一次性支付 25 美元( support)。谷歌。com/Google play/Android developer/answer/6112435。从处理开始,我们需要做的就是选择文件菜单下的“导出签名包”选项(图 3-8 )。
图 3-8。
“Export Signed Package” option in the PDE’s File menu
选择此选项后,处理将要求创建一个新的密钥库来保存发布密钥,以便对应用包进行签名。密钥库需要密码和关于密钥库发行者的附加信息(名称、组织、城市、州、国家),尽管这些是可选的。允许我们输入所有这些信息的密钥库管理器窗口显示在图 3-9 中。
图 3-9。
Entering the information needed to create a keystore in Processing
请记住此密码,因为每次导出新的签名包时都必须使用它。尽管您可以重置它并创建一个新的密钥,但您应该记住,一旦应用上传到 Play Store,您就不能更改密钥——应用的任何后续更新都需要使用与原始密钥相同的密钥进行签名,否则它将被拒绝,您必须使用新密钥创建一个新包。
已签名(并对齐)的包将保存在草图文件夹内的 build 子文件夹中,名为[Sketch name in lowercase]_release_signed_aligned.apk。一旦我们有了这个包,我们就可以按照谷歌的指示来完成应用发布过程: https://support.google.com/googleplay/android-developer/answer/113469 。
如果我们按照葡萄藤草图的所有这些步骤,我们应该能够生成一个签名包,准备上传到 Play Store。我们也可以使用 adb 工具在我们的设备上手动安装它(参见图 3-10 )。
图 3-10。
Installing a signed package from the command line using adb
如果我们手动或通过 Play Store 安装最终的应用包,我们应该会在应用启动器中看到我们为其创建的图标(图 3-11 )。
图 3-11。
The vine-drawing app installed on our device
摘要
本章涵盖了许多技术主题,包括使用 Processing 的控制台、集成调试器或 adb 中的 logcat 选项调试我们的代码;根据设备的 DPI 缩放草图的输出;并将我们的草图导出为签名包,准备上传到 Play Store。有了这些工具,我们已经准备好与全世界的 Android 用户分享我们的创作了!
四、绘制图形和文本
在这一章中,我们将深入探讨用于绘制形状、图像和文本的处理 API,使用几个代码示例来说明 API 中的不同功能。我们还将学习如何使用 P2D 渲染器和PShape类来获得更好的 2D 性能。
正在处理的渲染器
在前面的章节中,我们学习了加工草图的基本结构,它由一个setup()和一个draw()函数组成,前者包含草图初始化,后者包含在每一帧中更新屏幕的代码。作为初始化的一部分,我们需要用size()函数指示输出区域的大小,如清单 4-1 所示。我们还看到,fullScreen()功能允许我们使用设备屏幕的整个区域,而不管其分辨率如何。
size()和fullScreen()函数都接受一个“渲染器”选项。渲染器是处理过程中的模块,它将草图中的绘制命令转换为设备屏幕上的最终图像。处理渲染器通过 Android 系统提供的 API(https://source.android.com/devices/graphics/)与图形硬件通信来实现这一点。
默认渲染器(JAVA2D)在没有给size()或fullScreen()额外选项时启用,使用 Android 的 Canvas API 并提供高质量的 2D 渲染。但是,性能可能会受到限制,尤其是在绘制许多形状和其他图形元素时。另外两个渲染器,P2D和P3D,通过 OpenGL API 使用图形处理单元(GPU),这带来了更高的性能,但代价是增加了电池消耗。我们可以通过调用带有适当参数的size()或fullScreen()来选择渲染器;例如,size(w, h)或size(w, h, JAVA2D)将使用默认渲染器生成草图,而size(w, h, P2D)或fullScreen(P2D)将启用 P2D 渲染器,如我们在清单 4-1 中所做的。无论我们使用 JAVA2D 还是 P2D,这个草图的输出都是一样的,但是在本章的后面我们会看到使用 P2D 的一些具体的优点。我们将在第十三章中介绍使用 P3D 渲染器绘制 3D 图形。
图 4-1。
Output of the full-screen P2D sketch
void setup() {
fullScreen(P2D);
background(255);
noFill();
rectMode(CENTER);
}
void draw() {
float w = 2*(width/2-mouseX);
rect(width/2, height/2, w, w/width * height);
}
Listing 4-1.Using Full-screen Output
with the P2D Renderer
绘制形状
第二章给我们概述了绘图 API 在处理中的一些重要元素。我们看到了如何使用像ellipse()或rect()这样的函数绘制原始形状,而任意形状可以使用beginShape()、vertex()和endShape()函数逐个顶点地创建。在这一节中,我们将更深入地了解形状绘制 API,并学习如何使用PShape类和通过将矢量图形加载到PShape对象中来将形状存储到对象中以便更快地渲染。
更多形状类型
让我们首先回顾一下我们所掌握的所有可能的形状类型。本质上,根据我们在beginShape()中指定的类型,顶点将以不同的方式连接以构建所需的几何图形,如图 4-2 所示。
图 4-2。
All the shape types available in Processing
用beginShape / endShape构造形状时,顶点的数量和顺序非常重要。图 4-2 描绘了每个顶点是如何根据类型合并到形状中的。如果我们以不同的顺序提供顶点——例如,如果我们在QUADS类型的形状中交换顶点 2 和 3——那么产生的形状看起来会扭曲。此外,每种类型(点和多边形除外)都需要特定数量的顶点来构建单独的形状;例如,3 × N 画 N 个三角形,4 × N 画 N 个四边形,等等。如果我们想创造复杂的形状,我们必须熟悉这些规则。例如,考虑在每种形状类型下,相同的顶点布局如何导致非常不同的视觉结果,如清单 4-2 所示。
图 4-3。
Outputs for different shape types
int[] types = {POINTS, LINES, TRIANGLES,
TRIANGLE_STRIP, TRIANGLE_FAN,
QUADS, QUAD_STRIP, POLYGON};
int selected = 0;
void setup() {
size(300, 300);
strokeWeight(2);
}
void draw() {
background(150);
beginShape(types[selected]);
for (int i = 0; i <= 10; i++) {
float a = map(i, 0, 10, 0, TWO_PI);
float x0 = width/2 + 100 * cos(a);
float y0 = height/2 + 100 * sin(a);
float x1 = width/2 + 130 * cos(a);
float y1 = height/2 + 130 * sin(a);
vertex(x0, y0);
vertex(x1, y1);
}
endShape();
}
void mousePressed() {
selected = (selected + 1) % types.length;
println("Drawing shape", selected);
}
Listing 4-2.Drawing Different Shapes of Different Types Using beginShape() and endShape()
Note
POLYGON类型是beginShape()的默认参数,所以如果我们不提供任何显式类型,我们将创建一个多边形。此外,多边形可以是开放的或封闭的,这可以用endShape(mode)控制,模式可以是OPEN或CLOSE。
曲线形状
到目前为止,我们一直在使用的vertex()函数允许我们向形状添加顶点,然后根据我们在beginShape()中选择的类型参数连接这些顶点。这种方法足够通用,可以生成我们能想到的几乎任何形状,甚至是弯曲的形状。在这种情况下,我们可以沿着数学曲线计算顶点的位置,然后将这些顶点添加到多边形形状中。例如,在清单 4-3 中,我们使用极坐标来生成一个随机的、看起来有机的形状。
图 4-4。
Three shapes created by our “organic shape” example
size(480, 480);
translate(width/2, height/2);
int numPoints = 100;
int degree = 5;
beginShape();
float[] coeffs = new float[degree];
for (int d = 0; d < degree; d++) {
coeffs[d] = random(0, 1);
}
float phase = random(0, TWO_PI);
for (int i = 0; i <= numPoints; i++) {
float theta = map(i, 0, numPoints, 0, TWO_PI);
float rho = 5;
for (int d = 1; d <= degree; d++) {
rho += coeffs[d - 1] * sin(d*theta+phase);
}
float x = 30 * rho * cos(theta);
float y = 30 * rho * sin(theta);
vertex(x, y);
}
endShape();
Listing 4-3.Creating a Curved “Organic” Shape with Polar Coordinates
在这个例子中,我们使用变量numPoints来设置要添加到形状中的点数。这个数字越高,曲线看起来越平滑。我们可以使用 Catmull-Rom 样条和 Bezier 曲线来代替,两者都给我们更直观的曲线控制,正如我们接下来将看到的。
我们通过重复调用curveVertex()函数,为样条需要经过的每个顶点调用一次,并通过设置其控制点,在形状内添加 Catmull-Rom 样条。这些控制点决定了样条曲线在其端点处的方向。Catmull-Rom 样条的一个方便的方面是它们通过所有的控制点,但是这些点和端点处的方向之间的关系不容易可视化。
让我们看看使用样条的细节。为了使代码更具可读性,我们将在处理中使用PVector类,它允许我们存储 2D 和 3D 位置,并执行基本的矢量代数。一个PVector对象有三个浮点字段——x、y、z——和一些计算方法,比如向量加法、减法、长度和航向角。《加工参考》有一节详细介绍了如何使用该功能( https://processing.org/reference/PVector.html ),还有一节教程( https://processing.org/tutorials/pvector/ )。在清单 4-4 中,我们使用了一个PVector对象数组来存储样条曲线经过的所有点。
size(480, 480);
PVector[] points = new PVector[11];
for (int i = 0; i <= 10; i++) {
if (i < 10) {
float a = map(i, 0, 10, 0, TWO_PI);
float r = random(100, 200);
points[i] = new PVector(r * cos(a), r * sin(a));
} else {
points[10] = points[0].copy();
}
}
translate(width/2, height/2);
fill(255);
beginShape();
for (int i = 0; i <= 10; i++) {
if (i == 0 || i == 10) curveVertex(points[i].x, points[i].y);
curveVertex(points[i].x, points[i].y);
}
endShape();
fill(0);
for (int i = 0; i <= 10; i++) {
ellipse(points[i].x, points[i].y, 10, 10);
}
Listing 4-4.Creating Catmull-Rom Splines with curveVertex()
我们首先创建一个 PVector 对象数组,用来存储曲线上的位置。因为我们正在创建一个封闭的形状,所以最后一个PVector是第一个的副本。然后,我们将存储在 PVector 数组中的位置作为曲线顶点添加到多边形形状中。线条if (i == 0 || i == 10) curveVertex(points[i].x, points[i].y);添加了对应于控制点的附加顶点,这些顶点被设置为与曲线中的第一个和最后一个点相同。尽管样条给了我们一条通过所有点的平滑曲线,我们可能会在第一点得到一个尖角,如图 4-5 所示。
图 4-5。
Output of the Catmull-Rom spline example
另一方面,由于控制点可用于调整曲线上每对顶点之间的曲率,贝塞尔曲线允许更直观地操纵形状。我们在第二章中应用了贝塞尔曲线来创建类似花和叶子的形状,我们可以在许多其他情况下使用它们。类似于我们之前的随机斑点,我们可以使用贝塞尔曲线创建一个随机形状。我们需要平滑地连接贝塞尔曲线;每一个都需要两个控制点和两个顶点。图 4-6 显示了如何共享相邻贝塞尔曲线的顶点和控制点,以确保整个曲线不包含尖角。
图 4-6。
Smoothly joining Bezier curves
与样条曲线一样,我们可以生成贝塞尔曲线将通过的点,方法是以等间距围绕形状的整个周长移动,然后构建切线方向,我们将沿着该方向放置连续曲线之间共享的控制点。这就是我们在清单 4-5 中所做的。
size(480, 480);
PVector[] points = new PVector[11];
PVector[] directions = new PVector[11];
for (int i = 0; i <= 10; i++) {
if (i < 10) {
float a = map(i, 0, 10, 0, TWO_PI);
float r = random(100, 200);
points[i] = new PVector(r * cos(a), r * sin(a));
directions[i] = PVector.fromAngle(points[i].heading() +
random(0, QUARTER_PI));
directions[i].mult(60);
} else {
points[10] = points[0].copy();
directions[10] = directions[0].copy();
}
}
translate(width/2, height/2);
strokeWeight(2);
fill(255);
beginShape();
for (int i = 0; i < 10; i++) {
vertex(points[i].x, points[i].y);
PVector CP1 = PVector.add(points[i], directions[i]);
PVector CP2 = PVector.sub(points[i+1], directions[i+1]);
bezierVertex(CP1.x, CP1.y, CP2.x, CP2.y, points[i+1].x, points[i+1].y);
}
endShape();
Listing 4-5.Creating a Bezier Curve with Consecutive Vertices
注意使用PVector的fromAngle()方法生成一个方向向量,方法是将位置向量旋转 0 到 90 度之间的随机量(QUARTER_PI,然后用一个常数因子(60)对其进行缩放。同样,最后一个矢量的位置和方向需要从第一个矢量复制过来,这样形状才能正确闭合。一旦所有这些向量被计算并存储在数组中,我们就用vertex()和bezierVertex()函数创建形状。
我们还可以在示例中添加一些额外的代码来绘制顶点和控制点。我们所要做的就是遍历点和方向数组,然后使用椭圆和直线来显示它们与形状的关系。清单 4-6 包含了这些额外的代码,我们将把它们粘贴到清单 4-5 的草图的末尾,得到如图 4-7 所示的输出。
图 4-7。
Shape obtained by joining Bezier consecutive curves, with their control points
strokeWeight(1);
for (int i = 0; i <= 10; i++) {
PVector prevCP = PVector.sub(points[i], directions[i]);
PVector nextCP = PVector.add(points[i], directions[i]);
stroke(0);
line(prevCP.x, prevCP.y, nextCP.x, nextCP.y);
noStroke();
fill(190, 30, 45);
ellipse(points[i].x, points[i].y, 10, 10);
fill(28, 117, 188);
ellipse(prevCP.x, prevCP.y, 7, 7);
ellipse(nextCP.x, nextCP.y, 7, 7);
}
Listing 4-6.Drawing the Control Points and Tangent Directions to a Bezier Curve
可以将曲线/贝塞尔曲线顶点与常规顶点组合在同一形状中。让我们用刚刚学的东西画一个简单的海景,用贝塞尔曲线造波。我在图 4-8 中勾勒出了这个想法。
图 4-8。
Pen and paper sketch of a seascape using Bezier curves
因为我们将使用相同的代码但不同的参数绘制几个形状,所以将每个形状存储在一个单独的对象中会很方便。为此,我们将定义一个包含一条波浪线的类,然后我们可以用它在setup()函数中创建几个波浪,如清单 4-7 所示。
void setup() {
fullScreen();
orientation(LANDSCAPE);
colorMode(HSB, 360, 100,100);
waves = new WavyLine[10];
for (int i = 0; i < 10; i++) {
float y = map(i, 0, 9, height * 0.85, height * 0.025);
color c = color(225, map(i, 0, 10, 30, 100), 90);
waves[i] = new WavyLine(y, c);
}
}
void draw() {
background(219, 240, 255);
for (int i = waves.length - 1; i >= 0; i--) {
waves[i].display();
}
}
class WavyLine {
int numDiv;
color fillColor;
PVector[] positions;
PVector[] directions;
WavyLine(float y, color c) {
numDiv = int(8 * displayDensity);
positions = new PVector[numDiv];
directions = new PVector[numDiv];
fillColor = c;
for (int i = 0; i < numDiv; i++) {
float x = 0;
if (0 < i) {
if (i == numDiv - 1) x = width;
else x = random(i/float(numDiv) * width * 1.2,
(i+1)/float(numDiv) * width * 0.8);
}
positions[i] = new PVector(x, y + random(-20, 20));
directions[i] = PVector.fromAngle(random(-0.5 * HALF_PI,
+0.5 * HALF_PI));
directions[i].mult(20 * displayDensity);
}
}
void display() {
noStroke();
fill(fillColor);
beginShape();
for (int i = 0; i < numDiv - 1; i++) {
vertex(positions[i].x, positions[i].y);
PVector cp1 = PVector.add(positions[i], directions[i]);
PVector cp2 = PVector.sub(positions[i+1], directions[i+1]);
bezierVertex(cp1.x, cp1.y, cp2.x, cp2.y,
positions[i+1].x, positions[i+1].y);
}
vertex(width, height);
vertex(0, height);
endShape();
}
}
Listing 4-7.Seascape Sketch, Using Objects to Draw Several Shapes
在这个例子中,我们使用内置的displayDensity变量,我们已经在第三章中讨论过,以确保贝塞尔曲线细分的数量和由控制点定义的切线向量的长度与屏幕的 DPI 成比例,这样我们就可以在不同的设备上获得一致的输出(图 4-9 )。
图 4-9。
Final result of the seascape sketch
在这个例子中,我们应该注意的一些额外的事情是在setup()中的orientation(LANDSCAPE)调用来强制草图以横向方向运行,以及使用 HSB 颜色空间来更容易地实现从较亮到较暗的蓝色色调的渐变。
形状属性
处理允许我们设置几个决定形状最终外观的属性。我们已经使用了填充和描边颜色属性,但是还有更多。例如,我们不仅可以设置线条的颜色,还可以设置线条的粗细(粗细)、端点和连接连续线段的连接点,如清单 4-8 所示。
size(800, 480);
float x = width/2;
float y = height/2;
stroke(0, 150);
strokeWeight(10);
strokeJoin(ROUND);
strokeCap(ROUND);
beginShape(LINES);
for (int i = 0; i < 100; i++) {
float px = x;
float py = y;
float nx = x + (random(0, 1) > 0.5? -1: +1) * 50;
float ny = y + (random(0, 1) > 0.5? -1: +1) * 50;
if (0 <= nx && nx < width && 0 <= ny && ny < height) {
vertex(px, py);
vertex(nx, ny);
x = nx;
y = ny;
}
}
endShape();
Listing 4-8.Setting Stroke Attributes
尝试使用不同的笔划连接值(MITER、BEVEL、ROUND)、端点值(SQUARE、PROJECT、ROUND)和权重值绘制草图,并与图 4-10 中所示的输出进行比较。
图 4-10。
Output of sketch demonstrating stroke attributes
尽管这些属性通常是为整个形状定义的(即,它们应用于形状中的所有顶点),但 P2D 和 P3D 渲染器允许您定义逐顶点属性。例如,可以为每个顶点单独设置填充颜色,然后处理将在中间位置插入颜色。清单 4-9 中草图的输出类似于第二章中的 HSB 色轮,但是这里我们不需要设置中间色,因为 P2D 渲染器会自动设置(图 4-11 )。
图 4-11。
Color wheel obtained by interpolation of the fill color
size(300, 300, P2D);
colorMode(HSB, 360, 100, 100);
background(0, 0, 100);
translate(width/2, height/2);
noStroke();
beginShape(TRIANGLE_FAN);
fill(TWO_PI, 0, 100);
vertex(0, 0);
for (int i = 0; i <= 10; i++) {
float a = map(i, 0, 10, 0, 360);
float x = 150 * cos(radians(a));
float y = 150 * sin(radians(a));
fill(a, 100, 100);
vertex(x, y);
}
endShape();
Listing 4-9.Using Color Interpolation in the P2D Renderer
形状样式
我们可以通过处理设置的所有形状属性决定了当前的“样式”正如我们之前看到的,填充颜色、笔画颜色、粗细、帽和连接都是样式属性。随着我们的草图变得越来越复杂,尤其是对象在绘制时会设置自己的属性,很容易会忘记当前的样式。
处理包括两个函数,pushStyle()和popStyle(),它们可以方便地管理我们形状的样式,特别是当我们在执行草图的不同点改变许多样式属性时。pushStyle()保存所有样式属性的当前值,而popStyle()将所有样式属性恢复到我们上次调用pushStyle()时保存的值。如果我们在连续的pushStyle()和popStyle()调用之间设置一种全新的风格,我们可以确定两种不同的风格不会混淆。清单 4-10 提供了这种技术的一个简单例子。
Circle[] circles = new Circle[100];
void setup() {
size(800, 800);
for (int i = 0; i < circles.length; i++) {
circles[i] = new Circle();
}
}
void draw() {
translate(width/2, height/2);
rotate(frameCount * 0.01);
for (int i = 0; i < circles.length; i++) {
circles[i].display();
}
}
class Circle {
float x, y, r, w;
color fc, sc;
Circle() {
x = random(-width/2, width/2);
y = random(-height/2, height/2);
r = random(10, 100);
w = random(2, 10);
fc = color(random(255), random(255), random(255));
sc = color(random(255), random(255), random(255));
}
void display() {
pushStyle();
stroke(sc);
strokeWeight(w);
fill(fc);
ellipse(x, y, r, r);
popStyle();
}
}
Listing 4-10.Saving and Restoring Styles with pushStyle() and popStyle()
形状轮廓
Processing API 中另一个有用的特性是使用等高线进行减法绘制。包含在beginContour()和endContour()函数之间的所有顶点定义了一个从较大形状中移除的负形状,如清单 4-11 所示,它生成了如图 4-12 所示的输出。
图 4-12。
Shape with holes created with beginContour/endContour
void setup() {
size(300, 300);
}
void draw() {
background(190);
translate(width/2, height/2);
float r = width/2;
beginShape();
circleVertices(0, 0, r, 0, TWO_PI);
makeContour(0, 0, r/4);
makeContour(-r/2, -r/2, r/4);
makeContour(+r/2, -r/2, r/4);
makeContour(-r/2, +r/2, r/4);
makeContour(+r/2, +r/2, r/4);
endShape();
}
void makeContour(float xc, float yc, float r) {
beginContour();
circleVertices(xc, yc, r, TWO_PI, 0);
endContour();
}
void circleVertices(float xc, float yc, float r, float a0, float a1) {
for (int i = 0; i <= 30; i++) {
float a = map(i, 0, 30, a0, a1);
vertex(xc + r * cos(a), yc + r * sin(a));
}
}
Listing 4-11.Making Holes Inside a Shape with beginContour() and endContour()
Note
轮廓只能用于POLYGON类型的形状。此外,轮廓中顶点的“缠绕”或方向(顺时针或逆时针)必须与包含形状的方向相反。
PShape 类
正如我们到目前为止所看到的,绘制形状包括在每一帧中重复调用beginShape()、vertex()和endShape()。我们可以预先计算坐标并将它们放入自定义类中,就像我们在海景示例中所做的那样;然而,Processing 已经提供了一个内置的类来保存形状数据,恰当地称为PShape。这个类不仅帮助我们保持代码更有组织性和可读性,还允许我们从矢量图形文件(SVG)中读取形状,并且在使用 P2D 渲染器时,提高我们草图的性能。
创建 PShapes
通过调用createShape()函数可以创建一个PShape对象,我们可以通过三种不同的方式向它传递适当的参数:
- 如果没有提供参数,
createShape()返回一个空的PShape,我们可以使用beginShape()、vertex(),和endShape()来构建一个定制的形状。 - 提供原始类型(
ELLIPSE、RECT等)。)和初始化原始形状所需的附加参数。 - 指定单个
GROUP参数,这会产生一个可用于包含其他形状(自定义或原始)的PShape。
一旦我们创建并正确初始化了PShape对象,我们就可以使用shape()函数任意多次绘制它,如清单 4-12 和图 4-13 所示。
图 4-13。
Primitive, custom, and group PShape objects
size(650, 200, P2D);
PShape circle = createShape(ELLIPSE, 100, 100, 100, 100);
PShape poly = createShape();
poly.beginShape(QUADS);
poly.vertex(200, 50);
poly.vertex(300, 50);
poly.vertex(300, 150);
poly.vertex(200, 150);
poly.endShape();
PShape group = createShape(GROUP);
group.addChild(circle);
group.addChild(poly);
shape(circle);
shape(poly);
translate(300, 0);
shape(group);
Listing 4-12.Creating and Drawing PShape Objects
我们可以使用在本章第一节中学到的所有形状绘制功能来创建自定义形状;唯一的区别是我们需要调用相应的PShape对象上的函数。
当我们需要处理一个在草图运行时不会改变的非常大的几何体时,组合形状非常有用。如果没有PShape,每一帧的顶点都会被复制到 GPU 内存中,这样会降低帧率。然而,如果我们将所有这些顶点打包在一个PShape中,并使用 P2D 渲染器,这个拷贝只发生一次,这会带来更好的性能。这对于受电池电量使用限制的移动设备尤其重要。作为一个例子,让我们考虑一下模式示例中演示|性能下的CubicGridImmediate(编号PShape)和CubicGridRetained ( PShape)内置示例的帧速率。两张草图创造了完全相同的几何图形,一个半透明立方体的 3D 网格;然而,第一个草图勉强超过每秒 10 帧(fps),而第二个草图在大多数设备上将以每秒 60 帧的速度运行。
尽管PShape几何体原则上必须是静态的,才能实现这些性能提升,但在一定程度上修改顶点颜色和位置仍然是可能的,而不会降低速度。如果我们的修改只应用于一个较大的组中的子形状的子集,性能应该保持很高。让我们看看清单 4-13 中的场景,它生成了图 4-14 中描述的输出。
图 4-14。
Modifying the fill color of a child shape inside a group
PShape grid, sel;
void setup() {
fullScreen(P2D);
orientation(LANDSCAPE);
grid = createShape(GROUP);
for (int j = 0; j < 4; j++) {
float y0 = map(j, 0, 4, 0, height);
float y1 = map(j+1, 0, 4, 0, height);
for (int i = 0; i < 8; i++) {
float x0 = map(i, 0, 8, 0, width);
float x1 = map(i+1, 0, 8, 0, width);
PShape sh = createShape(RECT, x0, y0, x1 - x0, y1 - y0, 30);
grid.addChild(sh);
}
}
}
void draw() {
background(180);
shape(grid);
}
void mousePressed() {
int i = int(float(mouseX) / width * 8);
int j = int(float(mouseY) / height * 4);
int idx = j * 8 + i;
sel = grid.getChild(idx);
sel.setFill(color(#FA2D45));
}
void mouseReleased() {
sel.setFill(color(#C252FF));
}
Listing 4-13.Modifying Attributes of Child Shapes After Creation
这里,一次只修改一个子形状,获得新的填充颜色。只有更新的信息才会传输到 GPU,从而保持性能稳定。然而,随着更多的形状被同时修改,这种增益将会减少,直到性能变得等同于没有任何PShape对象的绘制。
Note
一个PShape对象的大多数属性在创建后都可以修改。在 https://processing.org/reference/PShape.html 可以找到所有可用的设置器功能。
从 SVG 加载形状
我们还可以通过使用loadShape()函数,使用PShape对象加载存储在文件中的几何图形。该函数接受 SVG 和 OBJ 格式,后者在 P3D 渲染器中受支持。SVG 代表可缩放矢量图形,SVG 格式的文件包含一个形状或一组形状的规范,其方式与处理几何图形的方式非常相似:作为顶点、样条或贝塞尔曲线的列表。
为了在我们的草图中加载一个 SVG 文件,我们首先需要将它放在草图的数据目录中。当我们在设备或仿真器上运行草图时,数据文件夹的所有内容都将被正确打包,以便可以从应用中访问它们。
Note
可以在草图的文件夹中手动创建数据目录。如果将媒体文件拖到 PDE 中,也会自动创建它。
一旦 SVG 被加载到一个PShape对象中,我们可以对它应用变换,比如平移或旋转,甚至改变它的样式属性,就像我们在清单 4-14 中所做的那样,在那里我们将三个 SVG 文件加载到不同的形状中(图 4-15 )。
图 4-15。
Loading, modifying, and displaying SVGs
size(450, 200, P2D);
PShape cc = loadShape("cc.svg");
PShape moz = loadShape("mozilla.svg");
PShape ruby = loadShape("ruby.svg");
translate(30, 50);
cc.setFill(color(170, 116, 0));
cc.setStroke(color(255, 155, 0));
shape(cc);
translate(cc.width + 30, 0);
shape(moz);
translate(moz.width + 30, 0);
shape(ruby);
Listing 4-14.Load SVG Files into PShape Objects
SVG 文件对于绘制难以单独通过代码生成的复杂几何图形非常有用。复杂 SVG 的另一个优点是,它们可以按层次方式组织,子形状在组内,允许单独操纵子形状。让我们看一个绘制世界地图 SVG 的例子,它也包含国家的名称(清单 4-15 )。运行它应该产生图 4-16 。
图 4-16。
Loading a map from an SVG file and selecting a country by its name attribute
PShape world;
void setup() {
size(950, 620, P2D);
world = loadShape("World-map.svg");
for (PShape child: world.getChildren()) {
if (child.getName().equals("algeria")) child.setFill(color(255, 0, 0));
}
}
void draw() {
background(255);
shape(world);
}
Listing 4-15.Selecting a Child Shape by Name and Setting Its Attributes
请注意我们是如何遍历所有子形状的,可以使用getChildren()函数从包含组中检索这些子形状作为一个PShape对象的数组。
绘制图像
在我们的应用中加载和显示图像文件是一个基本功能,处理起来非常简单。处理支持 GIF、JPG、TGA 和 PNG 图像格式,并包括一个内置的类PImage,用于处理草图中的图像。PImage封装图像的所有信息,包括宽度、高度和单个像素。加载和显示图像可以通过两个函数来完成,loadImage()和image(),如清单 4-16 所示。
fullScreen();
PImage img = loadImage("paine.jpg");
image(img, 0, 0, width, height);
Listing 4-16.Loading and Displaying an Image
image()函数最多接受四个参数:屏幕上图像的 x 和 y 坐标,以及显示图像的宽度和高度。这些宽度和高度参数不需要与图像的原始分辨率相同,它可以从PImage对象中的PImage.width和PImage.height变量获得。不带宽度和高度参数调用image()会导致图像以其源分辨率绘制,这相当于调用image(img, 0, 0, img.width, img.height)。
我们可以使用tint()函数将色调应用于整个图像,然后使用noTint()将其移除(否则,随后显示的所有图像将具有相同的色调,因为色调是另一种样式属性)。清单 4-17 举例说明了tint()和noTint()的使用,其输出如图 4-17 所示。
图 4-17。
Output of displaying an image with three different tints and no tinting
PImage img;
void setup() {
size(800, 533);
img = loadImage("paine.jpg");
}
void draw() {
image(img, 0, 0, width/2, height/2);
tint(255, 0, 0);
image(img, width/2, 0, width/2, height/2);
tint(0, 255, 0);
image(img, 0, height/2, width/2, height/2);
tint(0, 0, 255);
image(img, width/2, height/2, width/2, height/2);
noTint();
}
Listing 4-17.Tinting an Image
Note
当加载图像或任何其他媒体文件(如 SVG)时,在setup()函数中这样做很重要,该函数仅在应用启动时调用。否则,图像将在每一帧中重复加载,使应用变慢,直至无法使用。
纹理形状
我们也可以使用图像文件纹理形状(只有在 P2D/P3D 渲染器)。纹理化本质上是指将图像环绕在形状周围,这样形状就不再是用单一颜色绘制的。这个过程要求我们指定图像的哪些部分对应于形状的每个顶点。纹理可能非常复杂,尤其是在处理 3D 中的不规则形状时。清单 4-18 展示了最简单的情况——纹理化一个矩形。
PImage img;
size(800, 533, P2D);
img = loadImage("paine.jpg");
beginShape();
texture(img);
vertex(100, 0, 0, 0);
vertex(width – 100, 0, img.width, 0);
vertex(width, height, img.width, img.height);
vertex(0, height, 0, img.height);
endShape();
Listing 4-18.Texturing a Rectangle with an Image Loaded from a File
正如我们在这个例子中看到的,我们需要为vertex()函数提供两个额外的参数。vertex(x, y, u, v)调用中的这些参数对应于纹理映射的 UV 坐标,并指示图像中的像素(u,v)将到达形状中的顶点(x,y)。渲染器将基于该信息确定所有其他像素-顶点对应关系。在我们的简单纹理代码中,结果如图 4-18 所示。
图 4-18。
Textured 2D shape
绘图文本
文本是图形编程的另一个基本元素。Processing 提供了几个功能来在草图中绘制文本,并通过使用不同的字体和调整属性(如大小和对齐)来控制文本的外观。在接下来的几节中,我们将研究其中的一些函数。
加载和创建字体
在加工草图中绘制文本的第一步是加载位图字体。字体将被存储在一个PFont变量中,如果我们想在不同的时间用不同的字体绘图,我们可以在同一个草图中的不同PFont变量之间切换。内置的字体创建工具(位于“工具|创建字体…”下)允许我们从运行处理的 PC 或 Mac 计算机上可用的字体创建新的位图字体。该工具的界面如图 4-19 所示。一旦我们选择了字体、所需的大小和文件名,我们点击 OK,工具将在草图的数据目录中生成一个扩展名为. vlw 的字体文件,准备使用。
图 4-19。
Font-creator tool in the PDE
为了加载我们的新字体并将其设置为当前字体,我们分别使用了loadFont()和textFont()函数。一旦我们设置了想要的字体,我们就可以使用text()功能在屏幕上的任何地方绘制文本。清单 4-19 展示了所有这些功能。
size(450, 100);
PFont font = loadFont("SansSerif-32.vlw");
textFont(font);
fill(120);
text("On Exactitude in Science", 40, 60);
Listing 4-19.Loading a bitmap font generated with the font creator tool
在text(str, x, y)调用中的x和y参数让我们设置文本的屏幕位置。使用默认的文本对齐选项,它们表示第一个字符左下角的位置。图 4-20 显示了我们的文本绘制草图的输出。
图 4-20。
Text output in Processing
创建一个. vwl 字体文件,然后将其加载到草图中的一个缺点是,因为字体是预先创建的,所以它必须包含所有可能的字符。这浪费了内存,尤其是如果我们最终只使用了其中的几个。或者,我们可以用createFont(name, size)函数动态创建字体,该函数接受系统范围的字体名称或 TrueType 的文件名。ttf)或 OpenType(。otf)字体,以及字体大小。只有草图中实际使用的角色才会被创建并存储在内存中。这显示在清单 4-20 中。
size(450, 100);
PFont font = createFont("SansSerif", 32);
textFont(font);
fill(120);
text("On Exactitude in Science", 40, 60);
Listing 4-20.Creating a Font on the Fly
Note
Android 提供了三种适用于任何应用的全系统字体:serif、sans-serif 和 monospaced。每种字体都有四种变体:普通、粗体、斜体和粗斜体,因此,例如,无衬线字体的字体名称是 SansSerif、SansSerif-Bold、SansSerif-Italic 和 SansSerif-BoldItalic。
如果我们不向text()提供任何其他参数,文本字符串将继续向右延伸,直到脱离屏幕,或者,如果字符串包含一个换行符(\n),则转到下一行。我们可以用四个参数设置一个矩形区域,x、y、w和h,如果需要的话,处理会通过将文本分成几行来自动容纳在矩形内,如清单 4-21 和图 4-21 所示。
图 4-21。
Text output in Processing fitted inside a rectangular area
size(900, 300);
PFont font = createFont("Monospaced", 32);
textFont(font);
fill(120);
text("...In that Empire, the Art of Cartography attained such Perfection " +
"that the map of a single Province occupied the entirety of a City, " +
"and the map of the Empire, the entirety of a Province.", 20, 20,
width - 40, height - 40);
Listing 4-21.Placing Text Inside a Rectangular Area
文本属性
除了字体的名称和大小,我们还可以控制文本对齐(LEFT、RIGHT、CENTER、BOTTOM、TOP)和文本行之间的行距(清单 4-22 )。图 4-22 显示了设置这些属性的结果。尽管在本例中我们只设置了水平方向的对齐,但我们也可以通过向textAlign() — CENTER、BOTTOM或TOP提供第二个参数来设置垂直方向的对齐。
图 4-22。
Drawing text with different fonts and attributes
size(900, 300);
PFont titleFont = createFont("Serif", 32);
PFont textFont = createFont("Serif", 28);
textFont(titleFont);
textAlign(CENTER);
fill(120);
text("On Exactitude in Science", width/2, 60);
textFont(textFont);
textAlign(RIGHT);
textLeading(60);
text("...In that Empire, the Art of Cartography attained such Perfection " +
"that the map of a single Province occupied the entirety of a City, " +
"and the map of the Empire, the entirety of a Province.",
20, 100, width - 40, height - 20);
Listing 4-22.Setting Text Alignment and Leading
缩放文本
在第三章中,我们看到了如何使用displayDensity变量根据设备的 DPI 缩放草图中的图形。这种技术允许我们在不同分辨率和屏幕尺寸的设备上保持一致的视觉输出,我们也可以在绘制文本时使用它。我们需要做的就是将字体大小乘以displayDensity,如清单 4-23 所示。我们在图 4-23 中看到,屏幕上的文本大小在三个具有不同 dpi 的设备上保持不变。我们在这个例子中引入的一个额外的函数是loadStrings(),它读取数据文件夹中的一个文本文件,并返回一个包含文件中所有文本行的字符串数组。
图 4-23。
From left to right, text output on a Samsung Galaxy Tab 4 (7”, 1280 × 800 px, 216 dpi), Nexus 5X (5.2”, 1920 × 800 px, 424 dpi), and a Moto E (4.3”, 960 × 540 px, 256 dpi)
fullScreen();
orientation(PORTRAIT);
PFont titleFont = createFont("Serif-Bold", 25 * displayDensity);
PFont bodyFont = createFont("Serif", 18 * displayDensity);
PFont footFont = createFont("Serif-Italic", 15 * displayDensity);
String[] lines = loadStrings("borges.txt");
String title = lines[0];
String body = lines[1];
String footer = lines[2];
textFont(titleFont);
textAlign(CENTER, CENTER);
fill(120);
text(title, 10, 10, width - 20, height * 0.1 - 20);
textFont(bodyFont);
text(body, 10, height * 0.1, width - 20, height * 0.8);
textAlign(RIGHT, BOTTOM);
textFont(footFont);
text(footer, 10, height * 0.9 + 10, width - 20, height * 0.1 - 20);
Listing 4-23.Scaling Font Size by the Display Density
摘要
这是一个很长的章节,但是我们涵盖了很多重要的概念和技术!基于我们在第二章中看到的 Processing 语言的介绍,我们现在已经学习了使用各种几何图形、样条和贝塞尔曲线绘制形状的细节;用不同的属性调整它们的外观;并用PShape类优化代码。除此之外,我们还学会了如何在草图中绘制图像和文本。通过将这些资源付诸实践,我们应该能够创建几乎任何我们能想到的视觉合成,并将其转化为 Android 应用。