Ribbon Line 生成艺术,基于 p5.js 的实现

最后更新于 2023-05-04

多彩的线条并排在画板上舞动,是不是看着很熟悉,这就是 ribbon line。本次教程我们将一起来实现基于 ribbon line 的生成艺术作品。

通过仔细观察,我们可以看出有如下几个特点:

  1. 多条线并行运动
  2. 线条的填充颜色是相间的,间隔填充了黑色,线条的颜色随着时间的推移而发生变化
  3. 线条局部来看是在做有规律的圆周运动
  4. 在运动的过程中,线条的运动方向会随机发生变化

基于上述特点,我们重点将其实现分为两个部分:

  1. 实现一条线的运动
  2. 在一条线的基础上绘制出多条线的运动

## 实现一条线的运动

### 圆周运动

一条线的运动基础是圆周运动,我们一起来看看如何基于 p5.js 绘制出一个圆周运动的轨迹。

如何实现一个最简单的圆周运动?我们在点和线的乐章中已经具体地做了说明,这里我们再来回顾一下下面这个示意图。

要让 A 点做圆周运动,我们只需要确定圆心,半径,运动的夹角,每次渲染循环时,增加点 A 运动的夹角即可。

下面是示例的代码

上面的代码中,我们定义了一个变量 theta,它代表了当前的夹角值,根据勾股定理,我们可以通过半径 radius 和夹角 theta,计算出特定夹角所对应的坐标点的位置。

1
2
3
4
5
6
7
8
// 圆半径
const radius = 10

// θ 对应的 X 坐标
const x = radius * cos(theta)

// θ 对应的 Y 坐标
const y = radius * sin(theta)

我们将此绘制过程进一步可视化一下

通过勾股定理,我们计算出了每一个夹角对应的位置,然后调用 line 方法,将上一次的位置和本次的位置连接成一条线段。当每一次渲染时 theta 变化量足够小的时候,最终就呈现出来一个圆形。

你可以试着修改一下上面的 theta += 0.01,让每次变化增大一些,看看有什么变化。

### 不规则运动

如果只是做简单的圆周运动,那么我们将会看到一个圆环,而不会有任何变化的效果。要让线条的运动中隐藏一些变化,我们需要在规律的运动中添加一些随机性。

为了实现这一点,我们可以在每次渲染时让圆心发生变化,使用每次运动的位置作为下一次的圆心。同时我们在每次渲染时,动态地修改半径值。

关键代码如下

1
2
3
4
5
6
7
lastPos.x = x
lastPos.y = y

centerPos.x = x
centerPos.y = y

radius = random(15, 35)
  1. centerPos.x 是绘制圆周运动的圆心点的横坐标,我们将本次位置的横坐标 x 赋值给它
  2. centerPos.y 是绘制圆周运动的圆心点的纵坐标,我们将本次位置的纵坐标 y 赋值给它

如此,在下一次渲染循环 draw 被调用时,圆周运动的圆心点就是上一次的位置了。

random 函数是 p5.js 的一个内置的获取随机数的函数,通过 random 函数我们获取一个介于 15 和 35 之间的随机数,并将其赋值给 radius,这样下一次渲染时,圆环就变得更加不规则,圆心和半径都发生了变化。

你可以试着修改一下 radius = random(15, 35) 这行代码中的随机数的范围,比如改成 -35 和 35,观察下会发生什么。

#### 让线条左右摇摆起来

现在我们的线条在做随机的圆周运动了,但是看起来很单调,和我们开篇的效果不一致,因为我们的角度变化目前是递增的,导致线条周而复始地一直往顺时针的方向运动。

要让线条随机地顺时针或者逆时针运动,我们需要对 theta += 0.2 这行代码进行一些修改,让它每次的变化量也是随机变化的。

我们引入一个 change = 1 的变量,让 change 变量随机变化为 1 或者 -1,并将 theta 修改为 theta += 0.2 * change,如此,theta 的值将会随机变大和缩小,当 theta 变大时,线条将顺时针运动,当其变小时,线条将逆时针运动。

在上面的代码中,我们重点来看看如下几行代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
let change = 1


theta += 0.1 * change

if (frameCount % 100 < random(0, 30)) {
    change = 1
    radius = random(5, 30)
} else if (frameCount % 100 > random(60, 100)) {
    change = -1
    radius = random(5, 30)
}
  1. 我们新定义了一个 change 的变量,并将其初始化为 1

  2. theta 角度的变化中,我们将每次的变化量和 change 结合起来,最终会动态地确定 theta 是变大还是变小

  3. 如何确定 change 是增加还是减少呢?我们使用了 frameCount 属性

    • frameCount 是 p5.js 里的一个内置属性,代表了当前渲染了多少次,即 draw 函数调用了多少次
    • frameCount % 100 是一个取余的操作,即 frameCount 除以 100 后的余数,由此我们得到了一个介于 0100 之间的整数
    • 然后我们将获得的余数和随机数进行对比,有 30% 的概率 change 会变为 1, 即线条以顺时针运动,有 40% 的概率 change 会变成 -1,即线条以逆时针运动,除此以外,线条保持之前的运动方向。

#### 让线条保持在画板内运动

现在线条可以随机的顺时针或者逆时针做随机圆周运动了,可是有时候移动的幅度太大,以至于线条都移动出画板可见区域了。我们需要对线条的位置做一些修正,当线条的位置移出画板后,将其重新定位到画板上去。

#### 多彩的线条

黑色的线条在白色的画板上舞动着,终归是有些单调,让我们给画板加点调料吧,这次我们将让线条的颜色随着时间的变化而发生渐变。

先来回顾一下颜色,我们在颜色之美这边文章中有详细介绍了颜色模式及其运用。我们知道在 p5.js 中,我们可以设置几种颜色模式 RGBHSBHSL,在本例中,我们将使用 HSB 模式。

如何将时间的变化映射到 HSB 模式呢?我们可以使用前面用到的 frameCount 属性,它代表着渲染次数,而渲染次数是和时间有关的。我们只需要将 frameCount 这个无限增长的整数映射到颜色色相的区间即可。

关键代码如下:

1
2
3
4
5
// 设置颜色模式和颜色分量的值
colorMode(HSB, 100)

// 设置颜色
stroke(frameCount * 0.05 % 100, 80, 100, 100)
  1. colorMode(HSB, 100) 是 p5.js 提供的设置颜色模式的方法,我们将颜色模式设置为 HSB 模式,并将每个颜色分量设置为 100
  2. stroke 是 p5.js 提供的设置绘图颜色的方法,我们可以基于这个方法来设置接下来的绘图颜色
  3. 我们在调用 stroke 方法时,传递了四个参数,即对应着 HSB 模式的四个颜色分量:hueSaturationBrightnessOpacity
  4. frameCount * 0.05 % 100 是我们实现的关键,我们先将 frameCount 进行了缩放,然后再除以 100 取余,为什么是除以 100 呢,因为我们在上一步设置了颜色的分量是 100,除以 100 再取余就可以将 frameCount 映射到 0 - 100 的色相区间了

下面是我们加上颜色后的代码和运行效果

可以看到,随着时间的流逝,线条舞动的颜色也在随之变化。

## 多条线同时运动

为了实现多条线同时运动,我们需要绘制几条平行线。我们的线段都是由点连接而成,所以我们只需要在每一个点绘制的同时,再多绘制几个平行的点即可。

那如何绘制几个平行的点呢,如下图所示:

当以点 O1 为圆心,绘制点 O2 时,我们同时绘制出另外的几个点即可,另外的几个点的坐标怎么确定呢?如图上的黄点和红点。我们可以看到图中,黄点、红点同点 O2 处于同一条线上,这条线就是 O1、O2 组成的向量的垂直向量 V12。知道了垂直向量 V12,我们就可以通过向量的运算,计算出让黄点和红点的位置了。

我们以黄点为例,从图中我们可以看出 黄点 Y2 的向量 = 点 O2 的向量 + 向量 O2Y2,而 O2 的向量是已经算出来了的(点 O2 的坐标),所以问题的关键是计算出 O2Y2 的向量,我们可以按照如下的步骤进行计算

  1. 计算出点 O1O2 组成的向量

    const vectorO12 = createVector(O2.x - O1.x, O2.y - O1.y)

  2. 计算出点 O1O2 向量的垂直向量 V12

    const vectorV12 = createVector(vectorO12.y, -vectorO12.x)

  3. 将垂直向量 V12 归一化,归一化后方便计算黄点的位置

    vectorV12.normalize()

  4. 计算点 O2 和黄点 Y2 之间的向量

    O2 和黄点 Y2 之间的距离即为线条的宽度,我们假设线条的宽度为 5,那么我们将 V12 归一化后的向量设置为 5 的长度即为 O2Y2 向量。

    vectorV12.setMag(5)

  5. 计算黄点的坐标向量

    从图中我们可以看出 黄点的向量 = 点 O2 的向量 + V12 向量

    const yellowVector = p5.Vector.add(O2 + vectorV12)

由此我们计算出黄点 Y2 的坐标向量,同理可计算出红点的坐标向量。

## 更进一步

本次教程,我们基于基础的向量、垂直向量、圆周运动以及随机游走模型,实现了一个多彩的 ribbon line 效果。

接下来,你可以做更多的尝试,比如下面一些可能的点:

  1. theta 每次的变化量增大
  2. 实现多条线的时候,尝试给每一条线设置不同的颜色
  3. 基于本教程实现一个彩虹的效果