本次教程我们将介绍如何基于 p5.js 来绘制一朵花儿,教程将会涉及到贝塞尔曲线、颜色的操作、三角函数以及坐标系的旋转、缩放等
更多关于 p5.js 和生成艺术的教程,请查阅 beauty-of-pixel.tech
画一个花瓣
花是由很多花瓣组成,我们先看看怎么画出一朵花瓣,花瓣的形状是不规则的,我们可以使用贝塞尔曲线来绘制。
简单地解释一下上面几行代码:
- setup 函数中
createCanvas
创建了一个和窗口一样大小的画布。noLoop(true)
禁用了渲染循环,让 draw 只会调用一次。fill
填充一个颜色
- draw 函数中
translate
将画布的原点转移到了画布的中心点。- 定义了贝塞尔曲线的起点、终点、控制点,并调用 bezier 进行绘制。
下面就来具体看看 bezier 曲线是如何通过几个点来绘制出上面的花瓣形状的。
贝塞尔曲线
贝塞尔曲线是一种数学曲线,由法国数学家皮埃尔·贝塞尔在19世纪提出。它是通过控制点来定义的,这些控制点决定了曲线的形状和方向。贝塞尔曲线在计算机图形学和艺术设计中被广泛使用,因为它们可以产生平滑的曲线和复杂的形状。本节内容将主要介绍二次贝塞尔曲线
和三次贝塞尔曲线
。
二次贝塞尔曲线
二次贝塞尔曲线由三个点来定义:起始点、控制点和终止点,这些点通过插值算法确定曲线的形状。
quadraticVertex 绘制
在 p5.js 中,我们可以通过 quadraticVertex
方法来定义顶点的坐标。如下面的示例:
在使用 quadraticVertex
时,需要先调用 beginShape 和 vertex 方法。beiginShape 表示开启图形的绘制,而 vertex 用来定义二次贝塞尔曲线的起始点。
插值法绘制
还可以通过对曲线点的差值方法来绘制二次贝塞尔曲线,二次贝塞尔曲线的数学表示如下:
其中 P0
是起始点,P1
是控制点,P2
是终止点。参数 t
的取值范围是 [0, 1] 闭合区间。
基于此公式,代入 x 坐标和 y 坐标,即可以通过以下坐标点公式计算出二次贝塞尔曲线每个点的坐标值:
通过遍历从 0 到 1 之间的 t 的值,就可以得到曲线上的点。
将其转换为代码如下:
三次贝塞尔曲线
bezier 方法绘制
和二次贝塞尔曲线相似,三次贝塞尔曲线包含了起始点、控制点 1、控制点 2 和终止点。在 p5.js 可以使用 bezier 方法来实现,如下代码:
上面的画布上绘制出了本节教程里的花瓣,同时上面显示有两个蓝色的控制点和一个红色的点(实际上有两个,因为起始点和终止点坐标一样,绘制在了同一个地方,所以看起来只有一个)。
使用 bezier
方法可以很方便地绘制出三次贝塞尔曲线,只需要传入 4 个坐标位置即可,除此之外也可以通过 bezierVertex
方法来绘制,类似于二次曲线的调用方式。
beginShape();
vertex(startPoint.x, startPoint.y);
bezierVertex(ctrlPoint1.x, ctrlPoint1.y, ctrlPoint2.x, ctrlPoint2.y, endPoint.x, endPoint.y);
endShape();
插值方法绘制
和二次贝塞尔曲线相似,也可以通过插值方法来绘制三次贝塞尔曲线。
三次贝塞尔曲线的数学公式如下:
其中 P0
是起始点,P1
是控制点,P2
是控制点,P3
是终止点。参数 t
的取值范围是 [0, 1] 闭合区间。
基于此公式,代入 x 坐标和 y 坐标,即可以通过以下坐标点公式计算出三次贝塞尔曲线每个点的坐标值:
通过遍历从 0 到 1 之间的 t 的值,可以获得曲线上的一系列点,从而绘制出整条曲线。
特别的,当把三次贝塞尔曲线的两个控制点设置为同一个点的时候,曲线即变为二次贝塞尔曲线,并且两者形状保持一致。
下面我们来实现一个贝塞尔曲线编辑器,帮助我们生成更多的花瓣形状。
绘制一层的花瓣
有了单个的花瓣,接下来就可以绘制一层花瓣了,一层花瓣即是将花瓣平均分布在圆周上。
通俗地说,就是将圆分成几等份,每等份上画一个花瓣即可,比如一层有 5 个花瓣,我们即是将圆分成 5 份,每份里绘制一个花瓣。
上述代码中,起到关键作用的是 rotate( 1 / petalCount * Math.PI * 2)
- rotate 是 p5.js 提供的旋转画布的方法,画布是以原点为中心点进行旋转的。
- draw 方法里的
translate(width / 2, height / 2)
即是将画布的默认的左上角原点转移到画布的中心点。 - 完整圆周的角度是
Math.PI * 2
,将其除以花瓣的个数,即可以得到每片花瓣所占的角度。
每绘制一个花瓣,就将圆周旋转每一份花瓣所占的度数。圆周的总度数是
Math.PI * 2
,那么每一份的度数即是1 / petalCount * Math.PI * 2
下面以动画的形式来看看一层花瓣的绘制过程
绘制多层花瓣
有了一层花瓣的绘制方法 drawLayerOfPetal 后,绘制多层花瓣就简单了,重复地调用绘制一层花瓣的方法即可。
通过多次调用 drawLayerOfPetal 函数,实现了多层花瓣的绘制,随着花瓣层数的增多,花瓣的长度也随之变小。
drawLayerOfPetal(240)
drawLayerOfPetal(192)
drawLayerOfPetal(144)
drawLayerOfPetal(96)
drawLayerOfPetal(48)
这里可以将其简化为一个 for 循环如下:
const layerCount = 5
for (let i = 0; i < layerCount; i++) {
drawLayerOfPetal(240 * (1 - i / layerCount))
}
每绘制一层花瓣,就让花瓣半径减少 1 / layerCount
,这里即减少 20%,第一层的半径是 240,也就是每绘制一层花瓣,花瓣半径减少 48。
让每层的花瓣有层次感
现在每一层花瓣都是重叠在一起的,为了让每层的花瓣有层次感,可以在绘制每一次花瓣之前,将画板进行一定角度的旋转,这样就可以让花瓣绘制在不同的位置上。
修改绘制每层花瓣的代码,在前面加上 rotate
对花瓣进行旋转。
const rotateRadian = 0.7
for (let i = 0; i < layerCount; i++) {
rotate(rotateRadian)
drawLayerOfPetal(radius * (1 - i / layerCount))
}
通过在绘制每层花瓣时将花瓣旋转 0.7,可以看到每一层的花瓣都是错开分布的了。
现在花瓣是处于静止状态,因为所有的数值都是写死的,可以引入部分变量进来,比如让每层花瓣旋转的角度发生改变,这样我们的花瓣就会动起来。
起到关键作用的是这三行代码
let rotateRadian = 0
let rotateRadianDelta = 0.001
rotateRadian += rotateRadianDelta
为了在每次运行时获取最新的旋转角度,我们将 rotateRadian
移到了 draw 函数的外部,并新添加了一个 rotateRadianDelta
变量,用于表示每次渲染后旋转角度的变化量。
在渲染结束后,rotateRadian 将被更新。在下次渲染时,画板将以上次旋转角度为基础进行再次旋转。
需要注意的是,在 p5.js 中调用 draw 函数进行绘制时,会先保存绘制环境(即 push() 操作),绘制完成后恢复绘制环境(即 pop() 操作)。
因此,在进行与画板相关的操作(如 translate、rotate 和 scale)时,如果需要保持视觉上的一致性,就需要将相关数据保存下来,如本例子中的
rotateRadian
值。
现在你可以试试修改一下上面的代码,比如将 layerCount = 5 改成更大的数值,点击运行看看会出现什么效果。
给花瓣加点颜色
到现在为止,花瓣已经完成了,但是看起来很单调,是时候来一点多姿多彩的花瓣了。
我们准备了两个颜色,一个是花瓣最底层的颜色,一个是花瓣最上层的颜色,然后在绘制每层花瓣的时候,获取两种颜色之间对应的插值来作为当前花瓣的颜色。
在 p5.js 里面获取颜色插值可以直接使用 lerpColor(c1, c2, amt)
方法,下面就来看看怎么使用:
在绘制每层花瓣的 for 循环中,通过 lerpColor
获取当前层花瓣的颜色值,然后在 drawPetal 函数中,就可以使用这个颜色值来绘制当前层花瓣。
可以看到现在我们的花瓣还有一个黑色的边框,这个可以通过 noStroke
函数把这个描边去掉。
更进一步
本教程我们基于贝塞尔曲线实现了一个花瓣的效果,涉及到了花瓣的旋转操作等。
你还可以基于本教程的代码做一些其他的修改,如下是一些可能的点:
- 最后我们的实现里,花瓣有个黑色的描边,看看怎么去掉它。
- 在教程中,我们实现了一个贝塞尔曲线来绘制花瓣的形状,看看怎么结合起来,实现不一样的花瓣形状。
- 花瓣的美丽在于颜色,看看怎么添加更多的颜色,来呈现更好看的视觉效果。