在 Processing 沙画系列开篇 中,小菜提到了沙画技法中的『漏』。
沙画技法中有一种方式叫『漏』,就是把沙子攥在手里并握紧拳头,靠拳头的松紧控制沙子的流量,线条会产生粗细的变化,同时在快速移动时,手的高低变化也会发生相应变化,此手法主要用来描绘图形。我们试试看能不能模拟出漏的感觉。
其实沙画的笔触模拟是非常复杂的,本篇我们来实现一个非常简单的笔触形式,也就是通过randomGaussian()
来模拟沙子的笔触分布情况。
知识小课堂-正态分布
我们先看下官方文档:
从平均值为 0 且标准差为 1 的随机数系列返回浮点数。每次调用 randomGaussian() 函数时,它都会返回一个符合高斯或正态分布的数字。理论上,randomGaussian() 可能返回没有最小值或最大值。相反,返回远离平均值的值的概率非常低。并且返回平均值附近的数字的概率更高。
"能不能说人话,我有些看不懂。。。"
要理解这个函数,需要理解『正态分布』这个概念。正态分布在日常生活中很常见,比如某个国家成年男性身高的分布、一个健康人在一天当中血压的变化、高考数学成绩的分布,这些数据的背后都隐藏着一个正态分布模型。
正态分布,就是在正常状态下的概率分布,而所谓分布,就是描述一组数中,有多少数是大,有多少数是小,这些大数和小数在整体中的占比又是多少。
小菜做了两个关于正态分布的 DEMO,一起来看看:
正态分布的整体图形曲线如下图:
描述正态分布,需要两个参数,一个就是峰值的位置,可以理解成一组数的平均值,一般用希腊字母 μ 表示,另外一个是分布的标准差,代表一组数的离散程度,一般用希腊字母 σ 来表示。
举个很简单的标准差的例子,如何衡量一个 NBA 球员的战斗力?
在 NBA 中,平均数据用来衡量一个球员的战斗力,比如场均得分,盖帽,抢断,助攻等。但是如果想知道哪位球员发挥最稳定该怎么办?在一些关键的比赛场合,你想要得分高,且发挥稳定的球员,而不是表现时好时坏,水平忽高忽低,波动很大的球员。
而标准差就是为了描述在一组数据中数据的波动大小而发明的。
Processing之randomGaussian()
Processing 的randomGaussian
返回的是从平均值为 0 且标准差为 1 的随机浮点数。通常我们在使用的时候,要乘以一个扩大的系数,假设为 scale,来获得一个从平均值为 0 且标准差为 scale 的随机数。
size(400, 400);
for (int y = 0; y < 400; y ) {
float x = randomGaussian() * 60;
line(200, y, 200 x, y);
}
这个例子来自官方 api 文档的一个例子,从例子中可以看到
- for 循环绘制,一共绘制了 400 个线段,得到了一组满足正态分布的数值
- 线段的长度是由
randomGaussian()
乘以了 60 得到,这个值带了正负符号,平均值是 0,标准差是 60
数学的东西,有时候不好理解。那么简单理解下,敲黑板了,划重点了:
在 Processing 中,使用 randomGaussian() * scale 来获得一个满足正态分布的随机值,当然正态分布是建立在一组数据之上的分布,单独讨论一个数字是没有意义的,我们通常可以用一个 for 循环中使用randomGaussian()
进行一组数据的生成。这组数据的大小分布规律,呈现两头低,中间高的特点。
仅理解这点,就已经足够让我们在生成艺术中施展拳脚了。
p5js中的randomGaussian
需要值得一提的是,Processing Java 中的randomGaussian
函数没有参数,默认是返回的平均值为 0,标准差为 1 的随机浮点数。
但在 p5js 中,randomGaussian
可以携带 0 个或者 1个 或者 2 个参数。它的函数签名是randomGaussian([mean], [sd])
,其中 mean 代表平均值,sd 代表标准差。两者用 [] 中括号扩起来,代表是可选的,可带也可不带的意思。
- 不带参数,表示返回的平均值为 0,标准差为 1 的满足正态分布的随机浮点数
- 带 1 个参数 mean,表示返回的平均值为 mean,标准差为 1 的满足正态分布的随机浮点数
- 带 2个参数 mean 和 sd,表示返回的平均值为 mean,标准差为 sd 的满足正态分布的随机浮点数
代码实现
终于到了代码实现环节了,完整代码如下:
代码语言:javascript复制int batchSandCount = 600;
float sandRange = 10;
final color SAND_COLOR_1 = #AC9730;
final color SAND_COLOR_2 = #B79733;
void setup() {
size(800, 800);
background(255);
}
void draw() {
if (!mousePressed) {
return;
}
float mouseXSpeed = abs(mouseX - pmouseX);
float mouseYSpeed = abs(mouseY - pmouseY);
float mouseSpeed = max(mouseXSpeed, mouseYSpeed);
mouseSpeed = constrain(mouseSpeed, 0, 100);
sandRange = map(mouseSpeed, 0, 100, 10, 100);
batchSandCount = int(map(mouseSpeed, 0, 100, 600, 1000));
for (int i = 0; i < batchSandCount; i ) {
float posx = randomGaussian() * sandRange;
float posy = randomGaussian() * sandRange;
if (random(1) < 0.5) {
stroke(SAND_COLOR_1);
} else {
stroke(SAND_COLOR_2);
}
point(mouseX posx, mouseY posy);
}
}
void keyPressed() {
if (key == 'c') {
background(255);
}
}
思路分析
声明下,以下思路仅小菜的一种思考实现方式,并不一定最好,但也算一种简单的模拟实现。
- 计算出鼠标的移动速度,取横向和纵向较大的速度,然后使用
constrain
函数限定移动的速度范围,防止过快的速度 - 我们模拟当手(鼠标)移动的速度和手中(笔触)沙子的数量成正比,当移动的越快时,手中流逝出的沙子数量就越多
- 我们模拟当手(鼠标)移动的速度和沙子的分布范围
sandRange
成正比,当移动的越快时,画布上的沙子分布的范围越大 - 使用了两种沙子颜色进行随机,来增强沙子的真实感
源码地址
Processing100天速写[1] 之 Day_055[2]
参考资料
[1]Processing100天速写: https://github.com/xiaocai-laoniao/Processing100DaysSketch
[2]Day_055: https://github.com/xiaocai-laoniao/Processing100DaysSketch/tree/main/Day_055
[3]https:/www.processing.love: https://www.processing.love