1.9 开始第一幅“码绘”——掌握大杀器”循环“,一招搞定百千万个懵逼脸

来源:互联网 发布:java登录界面与数据库 编辑:程序博客网 时间:2024/05/18 09:10

引言:重复与美

这一节里,我们将要学到一项强大的技能——循环
有了循环,我们就能够指挥计算机帮我们去做大量重复性的事情。

直觉上,提到“重复”,往往联想到枯燥乏味。的确,从操作而言,重复性地做单调乏味地任务绘让人感到烦躁不安。
然而,重复却又是不可避免的。而且,很有有趣的东西也是通过重复操作实现的。
在绘画中,重复性也体现得非常多。并且相当多的画作都是通过“重复”来体现其趣味性。
为此,我们先赏析一些画作。

埃舍尔



这一幅是荷兰著名画家埃舍尔的作品。
从它的结构可以明显看出,这就是将平面空间分割成了重复的结构。实际上每一块贴砖都是完全一样的。



这幅作品在重复中增添了一定的变化,这种变化制造了强烈的趣味感,让观赏者总有一种好奇心,想去搞清楚它的规律。


这幅作品在重复中增加了尺度的变化,它实际上是一幅“分形“(fractals)图案。

草间弥生

下面再看看怪婆婆草间弥生的作品。

这幅照片中,她的服饰/饰品/盒子/桌面/背景全都是她本人的标志性图案”波点“。
她作品的最大特色就是不断重复的”波点”。然而,仔细观察,其实每一样样物品上的波点都有所不同,波点的形状/颜色/排列方式都有所变化。通过这些重复中的变化,将最简单元素“点”构成了丰富的形态。
再看其它作品,虽然都是“波点”,都是重复,但却样样不同!各有各的趣味。









梵高

上述两位都是搞“图案”的。图案也是很容易联系上“重复”一词。但是,在表意性绘画中,重复也同样重要。
我们再看几幅梵高的作品。




仔细观察它们,可以看出,梵高也是一位善用“重复”的大师。他喜欢用重复的图案来填充平面区域,他喜欢用重复的笔触来表现对象。
但是,他在重复的过程中,却注入了强烈的即兴表现。他的每一个笔触都有所不同,其中既有他主观的控制,也有他当时的情绪/下意识/动作的不确定性。

分形艺术

近几十年来,还有一门专门运用”重复“的美术流派——分形艺术。
下面时几幅分形艺术图案。





另外附一个程序驱动的动图:
https://www.shadertoy.com/view/4df3Rn

仔细看这些图片,发现它们的美感完全来自于重复,而且主要是两种特征:1.局部和整体重复;2.重复中略有变化。

Craig Mullins

可别以为只有非写实绘画才喜欢重复。下面这位搞电脑绘画的,风格方面主要时写实的。但又在写实中加入了强烈的写意性。


观察这幅宏大的场景,可以发现,景观结构都是重复的:道路/灯/远处的建筑物/建筑物的细节部件等等。但在这种重复的有序结构中,整体上存在一种变化的节律,而且重复的细节结构也包含变化,例如每一个灯都略有不同。



在这一幅《狙击手》作品中,也存在大量重复结构,而且他的笔触其实也是重复的。同样的,这些重复性中由融入了节律和变化,从而制造了极大的丰富性。

第四维上的重复

第四维即时间(根据爱因斯坦相对论),在时间上的重复,在传统静态绘画中是不可能实现的,但用编程就可以做到,请看下面作品:


先看静止状态:





怎么样,很无聊吧。。。

下面再看看动起来的样子:





是否感到突然就有了耐人寻味的趣味?

这么比较,是希望读者明白,可以将动态看作一种全新的“媒介”,它可以用于表现以往的静态构成方式所无法表达的美感。


看了这么多例子,相信大家也明白了,重复本身就是一项高级的绘画手法,只要运用得当,可以制造出极大的丰富性/特殊的风格/引人入胜的趣味感。

这里,笔者总结一下用重复制造“美”的基本法则:

雷同的重复导致无聊,变化的重复制造美。

然而,重复虽然很棒,但是要操作重复却又是一件很恼火的事情。

好消息是,现在有了计算机,有了这个”俯首甘为孺子牛“的劳模,我们就有了更好的驾驭”重复“的帮手了,计算机最擅长的就是干这种重复的劳动。
为了让计算机听命于我们,我们必须要学会给他下命令,也就是我们即将要学会的强大技能——循环

用while循环来绘制懵逼脸的头发

循环有几种写法,最简单的即while循环。
我们先直接用它来改造之前的drawConfuseFace()函数,用其实现重复绘制一簇头发:

   1   2   3   4   5   6   7   8   9  10  11  12  13  14  15  16  17  18  19  20  21  22  23  24  25  26  27  28  29  30  31  32  33  34  35  36  37  38  39  40  41  42  43  44  45  46  47  48  49  50  51  52  53  54  55  56  57  58  59  60  61  62  63  64  65  66  67  68  69  70  71  72  73  74  75  76  77  78  79  80  81  82  83  84  85  86  87  88  89  90  91  92  93  94  95  96  97  98  99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120
// 函数setup() : 准备阶段
function setup() {
// 创建画布,宽度640像素,高度480像素
// 画布的坐标系统为,左上角坐标为(0,0),
// x方向水平向右,y方向垂直向下,单位像素
createCanvas(600,400);
}

// 函数draw():作画阶段
function draw() {
// 在数百哦位置画懵逼脸
drawConfuseFace(mouseX,mouseY,200,0.4,0.2,0.3);
}

// 画懵逼脸
function drawConfuseFace(
posX, posY, // 脸部中心位置
faceSize, // 脸部尺寸
scaleMouth, // 嘴巴尺度比例,相对于脸部尺寸
scaleLEye, // 左眼尺度比例, 相对于脸部尺寸
scaleREye) // 右眼尺度比例, 相对于脸部尺寸
{
// -------------- 1 画脸 ---------------
fill(255);// 填充白色
ellipse(posX,posY,faceSize,faceSize);// 圆圈
// -------------- 2 画眼睛 ---------------
// 2.1 计算眼睛相对于脸中心点的偏移量
var EyeOffsetX = 0.2 * faceSize; // 眼睛横向偏移量为脸部尺寸的0.2倍
var EyeOffsetY = 0 * faceSize; // 眼睛纵向偏移量为脸部尺寸的0倍

// 2.2 计算眼睛尺寸
// 左右眼尺寸
var LEyeSize = faceSize * scaleLEye;
var REyeSize = faceSize * scaleREye;
// 左右眼珠尺寸
var LIrisSize = LEyeSize * 0.4;
var RIrisSize = REyeSize * 0.4;
// 2.2 画出左眼
fill(255);// 填充白色
ellipse(
posX-EyeOffsetX, // 脸的中心位置向左偏移EyeOffsetX
posY+EyeOffsetY, // 脸的中心位置向下偏移EyeOffsetY
LEyeSize,
LEyeSize);
// 2.3 画出右眼
fill(255);// 填充白色
ellipse(
posX+EyeOffsetX,
posY+EyeOffsetY,
REyeSize,
REyeSize);
// 5 左眼珠
fill(0);// 填充黑色
ellipse(
posX-EyeOffsetX, // 位置与左眼一样
posY+EyeOffsetY,
LIrisSize, // 尺寸则采用比左眼小的尺寸
LIrisSize);
// 6 右眼珠
fill(0);// 填充黑色
ellipse(
posX+EyeOffsetX,
posY+EyeOffsetY,
RIrisSize,
RIrisSize);
// -------------- 3 画嘴巴 ---------------
// 3.1 计算嘴巴相对于脸部中心位置的偏移量
var MouthOffsetX = 0.0;
var MouthOffsetY = 0.3*faceSize;

// 3.2 计算嘴巴尺寸
var MouthWidth = faceSize * scaleMouth;
var MouthHeight = MouthWidth/2.0;
// 3.3 画出嘴巴
fill(255); // 填充白色
ellipse(
posX + MouthOffsetX,
posY + MouthOffsetY,
MouthWidth,
MouthHeight);
// -------------- 4 画头发 ---------------
var offsetX01 = -0.3;
// while(bool Condition){}:
// 当条件满足时,就执行{}内容,然后再判断()的条件
while(offsetX01<0.4)
{
drawOneHair(posX,posY,faceSize,offsetX01);
// 警戒:必须要让while()括号中的条件会变成false,否则,
// 就会形成死循环,程序陷入永世轮回,
// 不断地执行{}中的语句而无法退出。
offsetX01 += 0.1; // += 运算符用途:A+=B 等价于 A=A+B;
}
}

// 绘制一根头发
function drawOneHair(
faceX,faceY, // 脸的中心位置
faceSize, // 脸的尺寸
offsetXOnFaceSize) // 头发X坐标的的偏移量,以脸部尺寸为单位尺寸
{
// ------------- 1 计算尺寸和位置 ---------//
// 头发相对脸部中心的Y偏移量
var HairOffsetY = faceSize * 0.3;
// 计算X偏移量
var offsetX = offsetXOnFaceSize * faceSize;
// 头发长度
var HairLength = faceSize * 0.4;

// --------------- 2 画头发 ---------------//
line(
faceX + offsetX,
faceY - HairOffsetY,
faceX + offsetX,
faceY - (HairOffsetY + HairLength) );

其绘制效果与之前的程序一样,即在鼠标位置画一个懵逼脸。
注意,这里画出一簇头发,但与之前相比,代码却精简了。



下面,重点观察代码中的加粗部分:
// -------------- 4 画头发 ---------------
var offsetX01 = -0.3;
// while(bool Condition){}:
// 当条件满足时,就执行{}内容,然后再判断()的条件
while(offsetX01<0.4)
{
drawOneHair(posX,posY,faceSize,offsetX01);
// 警戒:必须要让while()括号中的条件会变成false,否则,
// 就会形成死循环,程序陷入永世轮回,
// 不断地执行{}中的语句而无法退出。
offsetX01 += 0.1; // += 运算符用途:A+=B 等价于 A=A+B;
}
其句法提炼出来即:
 1 2 3 4
while(bool Condition)
{
// 重复执行的内容
}

其执行流程图示如下:


在使用while循环的时候,通常而言,需要注意:
  • ()中不能直接用常量true,这是为了避免陷入死循环,永世不得超生;
  • 在{}中要保证()中的条件在有限次执行重复内容会变为false,这也是为了避免陷入永世轮回。

看来,循环这一招虽然强大,但也有陷入死循环的风险。不过,还有一个技巧避免死循环,用break;语句。
在while循环的执行代码中,即{}中,可以加入一句break; 其作用是直接退出循环。
在刚刚开始使用while循环时,要倍加小心,避免陷入死循环,其实可以始终采取下列套路作为开端:

  1  2  3  4  5  6  7  8  9 10 11 12
var count = 0; // 用count来对循环次数计数,若计数过大,则直接退出循环
while(bool Condition)
{
// 实际运行的内容,代码略
// 这里写想要重复做的内容,要考虑在有限循环次数下让Condition变为false
// 用if语句判断是否该直接退出循环
if(count>99999) // 这里99999可以替换为一个比较大的数,其实就是限制while循环最大循环次数
{
break;
}
count ++; // 用自增运算符++,让count增加1
}

注意,这个初学者套路中,有两个技巧:
在while循环中嵌套了if语句,这就说明,循环和分支流程是可以自由嵌套使用的;

循环的另一招:do-while

while循环在执行时,有可能一次也不执行{}中的内容。为了实现至少做一次{}中的内容,可以换用do-while循环,其句法如下:
 1 2 3 4 5 6
do
{
// 重复执行内容
// 这部分内容至少会执行一次
}
while(bool Condition)
其运算流程如下:




用for循环绘制一排懵逼脸

下面,就是循环的终极奥义——for循环。我们即将用它来画一群懵逼脸。

首先,我们先用for循环画一排懵逼脸,代码如下:
   1   2   3   4   5   6   7   8   9  10  11  12  13  14  15  16  17  18  19  20  21  22  23  24  25  26  27  28  29  30  31  32  33  34  35  36  37  38  39  40  41  42  43  44  45  46  47  48  49  50  51  52  53  54  55  56  57  58  59  60  61  62  63  64  65  66  67  68  69  70  71  72  73  74  75  76  77  78  79  80  81  82  83  84  85  86  87  88  89  90  91  92  93  94  95  96  97  98  99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132
// 函数setup() : 准备阶段
function setup() {
// 创建画布,宽度640像素,高度480像素
// 画布的坐标系统为,左上角坐标为(0,0),
// x方向水平向右,y方向垂直向下,单位像素
createCanvas(600,400);
}

// 函数draw():作画阶段
function draw() {
// 用变量x作为循环变量
for(var x=50;x<640;x+=100)
{
var y = 200; // 每一个懵逼脸的纵向位置固定为200
drawConfuseFace(x,y,120,0.4,0.2,0.3); // 用循环变量x来指定位置
}
}

// 画懵逼脸
function drawConfuseFace(
posX, posY, // 脸部中心位置
faceSize, // 脸部尺寸
scaleMouth, // 嘴巴尺度比例,相对于脸部尺寸
scaleLEye, // 左眼尺度比例, 相对于脸部尺寸
scaleREye) // 右眼尺度比例, 相对于脸部尺寸
{
// -------------- 1 画脸 ---------------
fill(255);// 填充白色
ellipse(posX,posY,faceSize,faceSize);// 圆圈
// -------------- 2 画眼睛 ---------------
// 2.1 计算眼睛相对于脸中心点的偏移量
var EyeOffsetX = 0.2 * faceSize; // 眼睛横向偏移量为脸部尺寸的0.2倍
var EyeOffsetY = 0 * faceSize; // 眼睛纵向偏移量为脸部尺寸的0倍

// 2.2 计算眼睛尺寸
// 左右眼尺寸
var LEyeSize = faceSize * scaleLEye;
var REyeSize = faceSize * scaleREye;
// 左右眼珠尺寸
var LIrisSize = LEyeSize * 0.4;
var RIrisSize = REyeSize * 0.4;
// 2.2 画出左眼
fill(255);// 填充白色
ellipse(
posX-EyeOffsetX, // 脸的中心位置向左偏移EyeOffsetX
posY+EyeOffsetY, // 脸的中心位置向下偏移EyeOffsetY
LEyeSize,
LEyeSize);
// 2.3 画出右眼
fill(255);// 填充白色
ellipse(
posX+EyeOffsetX,
posY+EyeOffsetY,
REyeSize,
REyeSize);
// 5 左眼珠
fill(0);// 填充黑色
ellipse(
posX-EyeOffsetX, // 位置与左眼一样
posY+EyeOffsetY,
LIrisSize, // 尺寸则采用比左眼小的尺寸
LIrisSize);
// 6 右眼珠
fill(0);// 填充黑色
ellipse(
posX+EyeOffsetX,
posY+EyeOffsetY,
RIrisSize,
RIrisSize);
// -------------- 3 画嘴巴 ---------------
// 3.1 计算嘴巴相对于脸部中心位置的偏移量
var MouthOffsetX = 0.0;
var MouthOffsetY = 0.3*faceSize;

// 3.2 计算嘴巴尺寸
var MouthWidth = faceSize * scaleMouth;
var MouthHeight = MouthWidth/2.0;
// 3.3 画出嘴巴
fill(255); // 填充白色
ellipse(
posX + MouthOffsetX,
posY + MouthOffsetY,
MouthWidth,
MouthHeight);
// -------------- 4 画头发 ---------------
var offsetX01 = -0.3;
// while(bool Condition){}:
// 当条件满足时,就执行{}内容,然后再判断()的条件
var count = 0;
while(offsetX01<0.4)
{
drawOneHair(posX,posY,faceSize,offsetX01);
// 警戒:必须要让while()括号中的条件会变成false
offsetX01 += 0.1; // += 运算符用途:A+=B 等价于 A=A+B;

if(count>99999)
{
break;
}
count ++;
}
}

// 绘制一根头发
function drawOneHair(
faceX,faceY, // 脸的中心位置
faceSize, // 脸的尺寸
offsetXOnFaceSize) // 头发X坐标的的偏移量,以脸部尺寸为单位尺寸
{
// ------------- 1 计算尺寸和位置 ---------//
// 头发相对脸部中心的Y偏移量
var HairOffsetY = faceSize * 0.3;
// 计算X偏移量
var offsetX = offsetXOnFaceSize * faceSize;
// 头发长度
var HairLength = faceSize * 0.4;

// --------------- 2 画头发 ---------------//
line(
faceX + offsetX,
faceY - HairOffsetY,
faceX + offsetX,
faceY - (HairOffsetY + HairLength) );
}


程序运行结果如下:


for循环的句法为:

for (语句 1; 语句 2; 语句 3)
{
    被执行的代码块
}


其执行流程如下:



for循环的句法结构理解起来比较困难。但是可以以最常见的套路来理解其用法:

 1 2 3 4 5 6
// 在()中使用一个循环变量
for (var i=0; i<10; i++)
{
// 重复行为
// 代码中可以对变量i进行读写
}

对应这个套路,循环流程可以图示为:



在上述示意的套路中,一般来说,不宜在{}中对循环变量i的取值进行改变,这样就保证了i的数值只在”语句3“发生变化。于是,循环体{}的执行次数就可以用i来完全限定了。在这个示例中,语句1将i初值赋为0,而语句2中条件是"i<10",且每一次运行语句3都对i的数值增加1,于是,只要不再{}中改变i的数值,则{}中代码的执行次数即为10次。

用for循环绘制一个懵逼脸方阵

在循环中可以继续嵌套循环。
比如,一种经典套路就是用两个for循环嵌套使用,从而绘制一个阵列的图形。

这个套路的代码如下:
 1 2 3 4 5 6 7 8
for(var col=0;col<MaxCol;col++) // 循环MaxCol次
// 第一层循环
{
// 每一次执行第一层循环时,都要完整执行完每一次第二层循环
for(var row =0;row<MaxRow;row++) // 循环MaxRow次
// 第二层循环
{
// 循环内容
// 这里往往都会使用循环变量col和row的值,但尽量不要去改变它们的数值
}
}

将这个套路运用到我们的懵逼脸程序中,就可以绘制一个懵逼脸方阵。
代码如下:

   1   2   3   4   5   6   7   8   9  10  11  12  13  14  15  16  17  18  19  20  21  22  23  24  25  26  27  28  29  30  31  32  33  34  35  36  37  38  39  40  41  42  43  44  45  46  47  48  49  50  51  52  53  54  55  56  57  58  59  60  61  62  63  64  65  66  67  68  69  70  71  72  73  74  75  76  77  78  79  80  81  82  83  84  85  86  87  88  89  90  91  92  93  94  95  96  97  98  99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136
// 函数setup() : 准备阶段
function setup() {
// 创建画布,宽度640像素,高度480像素
// 画布的坐标系统为,左上角坐标为(0,0),
// x方向水平向右,y方向垂直向下,单位像素
createCanvas(600,400);
}

// 函数draw():作画阶段
function draw() {
// 用变量col和row作为循环变量
for(var col=0;col<8;col++) // 画8列
{
for(var row =0;row<6;row++) // 画6行
{
var x = col*70; // x坐标为i的70倍,即X方向间隔为70像素
var y = row*60; // y坐标为i的60倍,即Y方向间隔为60像素
drawConfuseFace(x,y,60,0.4,0.2,0.3); // 用循环变量x来指定位置
}
}
}

// 画懵逼脸
function drawConfuseFace(
posX, posY, // 脸部中心位置
faceSize, // 脸部尺寸
scaleMouth, // 嘴巴尺度比例,相对于脸部尺寸
scaleLEye, // 左眼尺度比例, 相对于脸部尺寸
scaleREye) // 右眼尺度比例, 相对于脸部尺寸
{
// -------------- 1 画脸 ---------------
fill(255);// 填充白色
ellipse(posX,posY,faceSize,faceSize);// 圆圈
// -------------- 2 画眼睛 ---------------
// 2.1 计算眼睛相对于脸中心点的偏移量
var EyeOffsetX = 0.2 * faceSize; // 眼睛横向偏移量为脸部尺寸的0.2倍
var EyeOffsetY = 0 * faceSize; // 眼睛纵向偏移量为脸部尺寸的0倍

// 2.2 计算眼睛尺寸
// 左右眼尺寸
var LEyeSize = faceSize * scaleLEye;
var REyeSize = faceSize * scaleREye;
// 左右眼珠尺寸
var LIrisSize = LEyeSize * 0.4;
var RIrisSize = REyeSize * 0.4;
// 2.2 画出左眼
fill(255);// 填充白色
ellipse(
posX-EyeOffsetX, // 脸的中心位置向左偏移EyeOffsetX
posY+EyeOffsetY, // 脸的中心位置向下偏移EyeOffsetY
LEyeSize,
LEyeSize);
// 2.3 画出右眼
fill(255);// 填充白色
ellipse(
posX+EyeOffsetX,
posY+EyeOffsetY,
REyeSize,
REyeSize);
// 5 左眼珠
fill(0);// 填充黑色
ellipse(
posX-EyeOffsetX, // 位置与左眼一样
posY+EyeOffsetY,
LIrisSize, // 尺寸则采用比左眼小的尺寸
LIrisSize);
// 6 右眼珠
fill(0);// 填充黑色
ellipse(
posX+EyeOffsetX,
posY+EyeOffsetY,
RIrisSize,
RIrisSize);
// -------------- 3 画嘴巴 ---------------
// 3.1 计算嘴巴相对于脸部中心位置的偏移量
var MouthOffsetX = 0.0;
var MouthOffsetY = 0.3*faceSize;

// 3.2 计算嘴巴尺寸
var MouthWidth = faceSize * scaleMouth;
var MouthHeight = MouthWidth/2.0;
// 3.3 画出嘴巴
fill(255); // 填充白色
ellipse(
posX + MouthOffsetX,
posY + MouthOffsetY,
MouthWidth,
MouthHeight);
// -------------- 4 画头发 ---------------
var offsetX01 = -0.3;
// while(bool Condition){}:
// 当条件满足时,就执行{}内容,然后再判断()的条件
var count = 0;
while(offsetX01<0.4)
{
drawOneHair(posX,posY,faceSize,offsetX01);
// 警戒:必须要让while()括号中的条件会变成false
offsetX01 += 0.1; // += 运算符用途:A+=B 等价于 A=A+B;

if(count>99999)
{
break;
}
count ++;
}
}

// 绘制一根头发
function drawOneHair(
faceX,faceY, // 脸的中心位置
faceSize, // 脸的尺寸
offsetXOnFaceSize) // 头发X坐标的的偏移量,以脸部尺寸为单位尺寸
{
// ------------- 1 计算尺寸和位置 ---------//
// 头发相对脸部中心的Y偏移量
var HairOffsetY = faceSize * 0.3;
// 计算X偏移量
var offsetX = offsetXOnFaceSize * faceSize;
// 头发长度
var HairLength = faceSize * 0.4;

// --------------- 2 画头发 ---------------//
line(
faceX + offsetX,
faceY - HairOffsetY,
faceX + offsetX,
faceY - (HairOffsetY + HairLength) );
}

程序运行效果如下:




每一个懵逼脸都完全一样,看起来显得单调,那么我们试试让它们略有不同。
这也时再实践引言部分提出的“法则”:雷同的重复导致无聊,变化的重复制造美。
这里,简单办法就是,每一次绘制时,都运用循环变量col和row来计算五官尺寸,从而每一个的尺寸都略有不同。
代码如下:
   1   2   3   4   5   6   7   8   9  10  11  12  13  14  15  16  17  18  19  20  21  22  23  24  25  26  27  28  29  30  31  32  33  34  35  36  37  38  39  40  41  42  43  44  45  46  47  48  49  50  51  52  53  54  55  56  57  58  59  60  61  62  63  64  65  66  67  68  69  70  71  72  73  74  75  76  77  78  79  80  81  82  83  84  85  86  87  88  89  90  91  92  93  94  95  96  97  98  99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147
// 函数setup() : 准备阶段
function setup() {
// 创建画布,宽度640像素,高度480像素
// 画布的坐标系统为,左上角坐标为(0,0),
// x方向水平向右,y方向垂直向下,单位像素
createCanvas(600,400);
}

// 函数draw():作画阶段
function draw() {
// 用变量col和row作为循环变量
for(var col=0;col<8;col++) // 画8列
{
for(var row =0;row<6;row++) // 画6行
{
var x = col*70; // x坐标为i的70倍,即X方向间隔为70像素
var y = row*60; // y坐标为i的60倍,即Y方向间隔为60像素
// 运用循环变量col和row计算五官尺寸
var faceSize = 40 + col*2 + row*2;
var mouthScale = 0.5 - (col * 0.03 + row * 0.03);
var lEyeScale = col * 0.02 + row * 0.01;
var rEyeScale = row * 0.01 + row * 0.02;
// 绘制懵逼脸
drawConfuseFace(
x,y,
faceSize,
mouthScale,
lEyeScale,
rEyeScale);
}
}
}

// 画懵逼脸
function drawConfuseFace(
posX, posY, // 脸部中心位置
faceSize, // 脸部尺寸
scaleMouth, // 嘴巴尺度比例,相对于脸部尺寸
scaleLEye, // 左眼尺度比例, 相对于脸部尺寸
scaleREye) // 右眼尺度比例, 相对于脸部尺寸
{
// -------------- 1 画脸 ---------------
fill(255);// 填充白色
ellipse(posX,posY,faceSize,faceSize);// 圆圈
// -------------- 2 画眼睛 ---------------
// 2.1 计算眼睛相对于脸中心点的偏移量
var EyeOffsetX = 0.2 * faceSize; // 眼睛横向偏移量为脸部尺寸的0.2倍
var EyeOffsetY = 0 * faceSize; // 眼睛纵向偏移量为脸部尺寸的0倍

// 2.2 计算眼睛尺寸
// 左右眼尺寸
var LEyeSize = faceSize * scaleLEye;
var REyeSize = faceSize * scaleREye;
// 左右眼珠尺寸
var LIrisSize = LEyeSize * 0.4;
var RIrisSize = REyeSize * 0.4;
// 2.2 画出左眼
fill(255);// 填充白色
ellipse(
posX-EyeOffsetX, // 脸的中心位置向左偏移EyeOffsetX
posY+EyeOffsetY, // 脸的中心位置向下偏移EyeOffsetY
LEyeSize,
LEyeSize);
// 2.3 画出右眼
fill(255);// 填充白色
ellipse(
posX+EyeOffsetX,
posY+EyeOffsetY,
REyeSize,
REyeSize);
// 5 左眼珠
fill(0);// 填充黑色
ellipse(
posX-EyeOffsetX, // 位置与左眼一样
posY+EyeOffsetY,
LIrisSize, // 尺寸则采用比左眼小的尺寸
LIrisSize);
// 6 右眼珠
fill(0);// 填充黑色
ellipse(
posX+EyeOffsetX,
posY+EyeOffsetY,
RIrisSize,
RIrisSize);
// -------------- 3 画嘴巴 ---------------
// 3.1 计算嘴巴相对于脸部中心位置的偏移量
var MouthOffsetX = 0.0;
var MouthOffsetY = 0.3*faceSize;

// 3.2 计算嘴巴尺寸
var MouthWidth = faceSize * scaleMouth;
var MouthHeight = MouthWidth/2.0;
// 3.3 画出嘴巴
fill(255); // 填充白色
ellipse(
posX + MouthOffsetX,
posY + MouthOffsetY,
MouthWidth,
MouthHeight);
// -------------- 4 画头发 ---------------
var offsetX01 = -0.3;
// while(bool Condition){}:
// 当条件满足时,就执行{}内容,然后再判断()的条件
var count = 0;
while(offsetX01<0.4)
{
drawOneHair(posX,posY,faceSize,offsetX01);
// 警戒:必须要让while()括号中的条件会变成false
offsetX01 += 0.1; // += 运算符用途:A+=B 等价于 A=A+B;

if(count>99999)
{
break;
}
count ++;
}
}

// 绘制一根头发
function drawOneHair(
faceX,faceY, // 脸的中心位置
faceSize, // 脸的尺寸
offsetXOnFaceSize) // 头发X坐标的的偏移量,以脸部尺寸为单位尺寸
{
// ------------- 1 计算尺寸和位置 ---------//
// 头发相对脸部中心的Y偏移量
var HairOffsetY = faceSize * 0.3;
// 计算X偏移量
var offsetX = offsetXOnFaceSize * faceSize;
// 头发长度
var HairLength = faceSize * 0.4;

// --------------- 2 画头发 ---------------//
line(
faceX + offsetX,
faceY - HairOffsetY,
faceX + offsetX,
faceY - (HairOffsetY + HairLength) );
}

其结果如下:




for循环比while循环和do-while循环具有更大的自由度。
其实,仅仅使用上述for循环的套路,即可满足大多数情况的使用需求。

此外,for循环还有一种 for-in 的形态,要用它,还需要初步进入“数据结构”这个技能分支,因此这里先略去不谈,待后续章节再掌握它。

让懵逼脸能与鼠标交互

尽管用循环可以很快画出一大堆懵逼脸,但与传统画法相比,这个作品本身还未有特别的亮点,因为全凭手绘也可以搞定!
现在我们再做一点改造,让作品可以体现码绘的两个特色——动态和交互!

第一,动态性。
我们希望这些懵逼脸可以随时间发生变化。
预期它们的嘴巴处于反复张开合拢的过程中,那么嘴巴的比例应该时一个随着时间而反复变化的量。

第二,交互性。
我们希望这些懵逼脸可以相应鼠标位置的变化。
预期它们可以感知到自己和鼠标位置的距离,并且根据距离远近来改变自己的形态。
在之前的教材中已经讲解到,在程序中可以随时获得鼠标位置,即访问变量mouseX和mouseY。
那么,我们现在就是要在绘制每个懵逼脸之前,计算其与鼠标位置的距离,并基于此计算脸的尺寸和五官比例。

先看看修改后的代码及效果,其中,红色代码为实现”动态性“的关键而蓝色代码为实现“交互性”的关键

   1   2   3   4   5   6   7   8   9  10  11  12  13  14  15  16  17  18  19  20  21  22  23  24  25  26  27  28  29  30  31  32  33  34  35  36  37  38  39  40  41  42  43  44  45  46  47  48  49  50  51  52  53  54  55  56  57  58  59  60  61  62  63  64  65  66  67  68  69  70  71  72  73  74  75  76  77  78  79  80  81  82  83  84  85  86  87  88  89  90  91  92  93  94  95  96  97  98  99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175
// 函数setup() : 准备阶段
function setup() {
// 创建画布,宽度640像素,高度480像素
// 画布的坐标系统为,左上角坐标为(0,0),
// x方向水平向右,y方向垂直向下,单位像素
createCanvas(600,400);
}

var mouthScaleBase = 0.25;
var mouthScaleBaseChange = 0.005;

// 函数draw():作画阶段
function draw() {
//background(255); // 刷新整个画布为白色

mouthScaleBase += mouthScaleBaseChange;
if(mouthScaleBase>0.5||mouthScaleBase<0.0)
{
mouthScaleBaseChange = -mouthScaleBaseChange;
}

// 用变量col和row作为循环变量
for(var col=0;col<8;col++) // 画8列
{
for(var row =0;row<6;row++) // 画6行
{
// 1 计算懵逼脸中心位置(x,y)
var x = col*70 ; // x坐标为i的70倍,即X方向间隔为70像素
var y = row*60 ; // y坐标为i的60倍,即Y方向间隔为60像素
// 根据位置(x,y)与鼠标位置(mx,my)的距离来计算五官尺寸

// 2 计算鼠标位置离这个懵逼脸的距离
// 用一种简单方法表示距离:
// 鼠标位置和脸部中心位置的横纵坐标的差值的绝对值的总和
var dx = abs(x-mouseX);
var dy = abs(y-mouseY);
var distance = dx + dy; // 距离
// 让距离不超过200
if(distance>200)
{
distance = 200;
}

// 3 根据距离来计算脸部和五官尺寸
var faceSize = 40 + distance*0.2;
var mouthScale = mouthScaleBase;
var lEyeScale = distance * 0.001;
var rEyeScale = 0.4 - distance * 0.001;
// 4 绘制懵逼脸
drawConfuseFace(
x,y,// 脸部中心位置(x,y)
faceSize,// 下列四个就是脸部和五官尺寸
mouthScale,
lEyeScale,
rEyeScale);
}
}
}

// 画懵逼脸
function drawConfuseFace(
posX, posY, // 脸部中心位置
faceSize, // 脸部尺寸
scaleMouth, // 嘴巴尺度比例,相对于脸部尺寸
scaleLEye, // 左眼尺度比例, 相对于脸部尺寸
scaleREye) // 右眼尺度比例, 相对于脸部尺寸
{
// -------------- 1 画脸 ---------------
fill(255);// 填充白色
ellipse(posX,posY,faceSize,faceSize);// 圆圈
// -------------- 2 画眼睛 ---------------
// 2.1 计算眼睛相对于脸中心点的偏移量
var EyeOffsetX = 0.2 * faceSize; // 眼睛横向偏移量为脸部尺寸的0.2倍
var EyeOffsetY = 0 * faceSize; // 眼睛纵向偏移量为脸部尺寸的0倍

// 2.2 计算眼睛尺寸
// 左右眼尺寸
var LEyeSize = faceSize * scaleLEye;
var REyeSize = faceSize * scaleREye;
// 左右眼珠尺寸
var LIrisSize = LEyeSize * 0.4;
var RIrisSize = REyeSize * 0.4;
// 2.2 画出左眼
fill(255);// 填充白色
ellipse(
posX-EyeOffsetX, // 脸的中心位置向左偏移EyeOffsetX
posY+EyeOffsetY, // 脸的中心位置向下偏移EyeOffsetY
LEyeSize,
LEyeSize);
// 2.3 画出右眼
fill(255);// 填充白色
ellipse(
posX+EyeOffsetX,
posY+EyeOffsetY,
REyeSize,
REyeSize);
// 5 左眼珠
fill(0);// 填充黑色
ellipse(
posX-EyeOffsetX, // 位置与左眼一样
posY+EyeOffsetY,
LIrisSize, // 尺寸则采用比左眼小的尺寸
LIrisSize);
// 6 右眼珠
fill(0);// 填充黑色
ellipse(
posX+EyeOffsetX,
posY+EyeOffsetY,
RIrisSize,
RIrisSize);
// -------------- 3 画嘴巴 ---------------
// 3.1 计算嘴巴相对于脸部中心位置的偏移量
var MouthOffsetX = 0.0;
var MouthOffsetY = 0.3*faceSize;

// 3.2 计算嘴巴尺寸
var MouthWidth = faceSize * scaleMouth;
var MouthHeight = MouthWidth/2.0;
// 3.3 画出嘴巴
fill(255); // 填充白色
ellipse(
posX + MouthOffsetX,
posY + MouthOffsetY,
MouthWidth,
MouthHeight);
// -------------- 4 画头发 ---------------
var offsetX01 = -0.3;
// while(bool Condition){}:
// 当条件满足时,就执行{}内容,然后再判断()的条件
var count = 0;
while(offsetX01<0.4)
{
drawOneHair(posX,posY,faceSize,offsetX01);
// 警戒:必须要让while()括号中的条件会变成false
offsetX01 += 0.1; // += 运算符用途:A+=B 等价于 A=A+B;

if(count>99999)
{
break;
}
count ++;
}
}

// 绘制一根头发
function drawOneHair(
faceX,faceY, // 脸的中心位置
faceSize, // 脸的尺寸
offsetXOnFaceSize) // 头发X坐标的的偏移量,以脸部尺寸为单位尺寸
{
// ------------- 1 计算尺寸和位置 ---------//
// 头发相对脸部中心的Y偏移量
var HairOffsetY = faceSize * 0.3;
// 计算X偏移量
var offsetX = offsetXOnFaceSize * faceSize;
// 头发长度
var HairLength = faceSize * 0.4;

// --------------- 2 画头发 ---------------//
line(
faceX + offsetX,
faceY - HairOffsetY,
faceX + offsetX,
faceY - (HairOffsetY + HairLength) );
}

运行效果如下:



动态的实现

从图中可见,动态性主要表现在懵逼脸们的嘴巴随着时间而反复张开合拢。
首先看看红色代码时如何实现动态的。

在draw()函数的定义之前,定义了两个变量:

var mouthScaleBase = 0.25;
var mouthScaleBaseChange = 0.005;


其中,mouthScaleBase就是用于指定嘴巴比例的变量,即之后的代码:
var mouthScale = mouthScaleBase;

由于这两个变量定义在函数之外,于是它们的作用域在整个程序,称这两个为“全局变量”。
任何位置都可以对他们进行读写操作在draw()函数中可以直接对它们进行读写,由此定义了它们的变化规律:

function draw() {
// ......

// 每次运行draw()都将对mouthScaleBase 和mouthScaleBaseChange进行如下运算:
mouthScaleBase += mouthScaleBaseChange; 
if(mouthScaleBase>0.5||mouthScaleBase<0.0)
{
mouthScaleBaseChange = -mouthScaleBaseChange;
}


//......
}

我们现在已经知道,draw()是一个反复调用的函数。于是,上述代码也将反复调用,每一次调用,
mouthScaleBase 的数值都会发生变化,其变化量就是mouthScaleBaseChange,于是,当mouthScaleBaseChange数值为正的时候,mouthScaleBase 即会增大,反之,若mouthScaleBaseChange数值为负时,mouthScaleBase 即会减小。
而变量mouthScaleBaseChange定义了mouthScaleBase在每一次draw()时的变化量,相当于mouthScaleBase的“变化率”。
这里用了一个条件判断语句来让mouthScaleBaseChange发生变化,其条件为“mouthScaleBase>0.5||mouthScaleBase<0.0”,翻译成口语即“当mouthScaleBase大于0.5或mouthScaleBase小于0的时候”,变化方式就是取反,即代码“mouthScaleBaseChange = -mouthScaleBaseChange;
注意,这里再次说明“=”的用途是“赋值”而非“等于”,这句的意思就是将mouthScaleBaseChange的负值-mouthScaleBaseChange赋值给它自己。

于是,变量mouthScaleBase 就会在0~5之间反复变化。由于其赋值给mouthScale 并用于绘制懵逼脸 drawConfuseFace()时指定嘴巴的比例,于是嘴巴就实现了随时间开合的动态。

交互性的实现

现在再观察蓝色代码,理解鼠标交互的实现机制。

其基本思路是,在绘制每一个懵逼脸时,都要计算出鼠标位置和它的距离,然后,根据距离计算脸部尺寸和五官比例,再按计算结果绘制。

要点1. 计算鼠标位置和懵逼脸的距离。
这里采取了一种简单方法,即球的鼠标和脸部中心点的横纵坐标差的绝对值dx和dy,这里用到了p5.js提供的计算绝对值的函数abs():
var dx = abs(x-mouseX); 
var dy = abs(y-mouseY);


并将二者相加,即代码:
var distance = dx + dy; // 距离

此外,为了让后续计算时,尺寸变化不至于过大,因此限制distance的取值再200以内,即代码:
// 让距离不超过200
if(distance>200)
{
distance = 200;
}





要点2. 根据距离计算脸部尺寸和五官比例。
在计算脸部尺寸和五官比例时,均用到了距离distance,即:

// 3 根据距离来计算脸部和五官尺寸
var faceSize = 40 + distance*0.2;
var mouthScale = mouthScaleBase;
var lEyeScale = distance * 0.001;
var rEyeScale = 0.4 - distance * 0.001;


这里采取的计算公式和数值其实都是按照一定的经验直觉来设定的,
而且均需要根据显示效果反复调整以达到最佳效果。

总结

到此为止,我们已经掌握的用JS配p5.js来进行“过程式编程“的基本方法。
从下一章开始,我们将深入p5.js这个宝库,并逐步探讨编程语言的高级技能。


知识点参考:

while循环和do-while循环: http://www.runoob.com/js/js-loop-while.html

for循环:http://www.runoob.com/js/js-loop-for.html