引言——如何绘制五官变化的懵逼脸?
调用前一节写好的画懵逼脸函数,能够控制的是它的位置。
如果我们希望不仅能在不同位置画懵逼脸,而且画出来的每一个懵逼脸的尺寸比例都有所不同,这样的函数就无法满足需求了。
为了满足这种变化的需求,我们可以对drawConfuseFace进一步改造,让其具有更多的可控性。
示例程序
下面先看看改造后的效果:
我们先直接看看实现上述效果的完整代码,然后再剖析其采用的改造措施:
// 函数setup() : 准备阶段
function setup() {
// 创建画布,宽度640像素,高度480像素
// 画布的坐标系统为,左上角坐标为(0,0),
// x方向水平向右,y方向垂直向下,单位像素
createCanvas(640,480);
}
// 函数draw():作画阶段
function draw() {
drawConfuseFace(200,140,200,0.3,0.2,0.2);
drawConfuseFace(320,280,150,0.1,0.2,0.15);
drawConfuseFace(100,300,100,0.1,0.3,0.3);
drawConfuseFace(480,120,220,0.5,0.25,0.25);
drawConfuseFace(520,340,160,0.7,0.5,0.5);
drawConfuseFace(mouseX,mouseY,120,0.4,0.3,0.12);
}
function drawConfuseFace(
posX, posY, // 脸部中心位置
faceSize, // 脸部尺寸
scaleMouth, // 嘴巴尺度比例,相对于脸部尺寸
scaleLEye, // 左眼尺度比例, 相对于脸部尺寸
scaleREye) // 右眼尺度比例, 相对于脸部尺寸
{
// -------------- 1 画脸 ---------------
fill(255);// 填充白色
ellipse(posX,posY,faceSize,faceSize);// 圆圈
// -------------- 2 画眼睛 ---------------
// 2.1 计算眼睛相对于脸中心点的偏移量
var EyeOffsetX = 0.2 * faceSize;
var EyeOffsetY = 0 * faceSize;
// 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,
posY+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 画头发 ---------------
drawOneHair(posX,posY,faceSize,-0.3);// 画第一根头发
drawOneHair(posX,posY,faceSize,-0.2);// 画第二根头发
drawOneHair(posX,posY,faceSize,-0.1);
drawOneHair(posX,posY,faceSize,0);
drawOneHair(posX,posY,faceSize,0.1);
drawOneHair(posX,posY,faceSize,0.2);
drawOneHair(posX,posY,faceSize,0.3);
}
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) );
}
作为对比,这里也贴出前一个版本的代码:
// 函数setup() : 准备阶段
function setup() {
// 创建画布,宽度640像素,高度480像素
// 画布的坐标系统为,左上角坐标为(0,0),
// x方向水平向右,y方向垂直向下,单位像素
createCanvas(640,480);
}
// 函数draw():作画阶段
function draw() {
// 在(200,140)位置画第一个脸
drawConfuseFace(200,140);
// 在(320,280)位置画第二个脸
drawConfuseFace(320,280);
}
function drawConfuseFace(posX, posY)
{
fill(255);// 填充白色
// 1 画脸
ellipse(posX,posY,200,200);// 圆圈
// 2 左眼
ellipse(posX-40,posY,50,50);// 另一个圆圈
// 3 右眼
ellipse(posX+40,posY,50,50);
// 4 嘴巴
ellipse(posX,posY+60,80,40);
fill(0);// 填充黑色
// 5 左眼珠
ellipse(posX-40,posY,20,20);
// 6 右眼珠
ellipse(posX+40,posY,20,20);
// 7 头发:从左到右画一排竖线
line(posX-60,posY-60,posX-60,posY-140);
line(posX-40,posY-60,posX-40,posY-140);
line(posX-20,posY-60,posX-20,posY-140);
line(posX ,posY-60,posX ,posY-140);
line(posX+20,posY-60,posX+20,posY-140);
line(posX+40,posY-60,posX+40,posY-140);
line(posX+60,posY-60,posX+60,posY-140);
}
示例程序解读——用函数参数、算符和表达式实现脸部比例计算
对比之前的代码,改进的程序主要是采取了两个措施:
- 措施1 增加了drawConfuseFace()函数的参数,增加的参数用于控制脸部的比例尺寸;
- 措施2 在函数代码中,运用算符和表达式,根据输入参数进行计算,得出五官的实际位置和尺寸。
先谈谈措施1。对比修改前后的drawConfuseFace()函数:
修改前:
function drawConfuseFace(posX, posY)
修改后:
function drawConfuseFace(
posX, posY, // 脸部中心位置
faceSize, // 脸部尺寸
scaleMouth, // 嘴巴尺度比例,相对于脸部尺寸
scaleLEye, // 左眼尺度比例, 相对于脸部尺寸
scaleREye) // 右眼尺度比例, 相对于脸部尺寸
其中关键在于多出来的几个参数: faceSize, scaleMouth, scaleLEye和scaleREye。它们就是用于计算脸部位置和五官比例尺寸的量。它们的含义入下图所示。
它的基本思想其实就是来自于绘画中的比例。在传统绘画技法中,画家需要研究常见事物的尺寸关系,例如,要画好人体,就要了解人体比例,像是三庭五眼、七头身等等说法都是与尺寸比例有关。下图就是达芬奇研究和绘制的人体比例有关的手稿:
再看措施2,其实就是在函数的执行代码中了实现了措施1要达到的设想。
通常而言,对于尺寸比例的计数都不采用绝对数值,而是相对数值,例如按”三庭五眼”比例而言,相当于表达的含义为:眼睛宽度为脸部宽度的1/5,只要给定了脸宽的具体尺寸数值,那么就可以计算出眼睛宽度的具体数值。在我们的drawConfuseFace函数中也是类似的含义:faceSize是脸部尺寸的具体数值,也就是脸部圆形的直径,而scaleMouth,scaleLEye和scaleREye则是嘴巴/左眼/右眼相对于脸部尺寸faceSize的比例值,即嘴巴/左眼/右眼的具体尺寸就可以用faceSize分别乘以这三个数值导出。例如,假定faceSize 为100像素,scaleMouth, scaleLEye和scaleREye的数值取0.4,0.3和0.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);
画左眼珠:
// 5 左眼珠
fill(0);// 填充黑色
ellipse(
posX-EyeOffsetX, // 位置与左眼一样
posY+EyeOffsetY,
LIrisSize, // 尺寸则采用比左眼小的尺寸
LIrisSize);
能够理解上述代码,再理解其它部分就相对容易了。
相对于眼睛和嘴巴,头发的定位和尺寸计算显得复杂一些:由于有很多根头发,每一根头发的绘制过程都是类似的,于是就将绘制一根头发的过程概括为新的函数drawOneHair():
// 绘制一根头发
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) );
}
其执行过程也类似于之前对于五官的定位和绘制:首先根据输入参数,计算出头发尺寸和位置,然后根据导出的数值,绘制出头发。为了大家理解,将计算尺寸和位置的代码翻译一下:
// 头发相对脸部中心的Y偏移量
var HairOffsetY = faceSize * 0.3;
翻译:头发底端相对于脸部中心的纵向偏移量HairOffsetY 等于脸部尺寸faceSize 的0.3倍。
// 计算X偏移量
var offsetX = offsetXOnFaceSize * faceSize;
翻译:头发底端相对于脸部中心的横向偏移量offsetX 等于比例值offsetXOnFaceSize乘以脸部尺寸faceSize。
// 头发长度
var HairLength = faceSize * 0.4;
翻译:头发长度等于脸部尺寸的0.4倍
然后,再将绘制头发的语句详细解读:
// --------------- 2 画头发 ---------------//
line(
faceX + offsetX,
faceY - HairOffsetY,
faceX + offsetX,
faceY - (HairOffsetY + HairLength) );
这里用到了p5.js提供的函数line(),其参考文档在:https://p5js.org/reference/#/p5/line
它有四个参数,前两个参数为线条的起点的坐标,后两个参数为线条末端坐标。
于是,线条的起点为(faceX + offsetX,faceY - HairOffsetY),翻译即“起点的横坐标为脸部中心横坐标faceX偏移offsetX,纵坐标向上偏移HairOffsetY”。线条末端为(faceX + offsetX,faceY - (HairOffsetY + HairLength)),翻译即“末端的横坐标为脸部中心横坐标偏移offsetX,纵坐标为脸部中心纵坐标faceY向上偏移HairOffsetY后再向上继续偏移头发长度HairLength那么多”。
图示它的计算方法如下:
表达式和运算符详解
在对尺寸和位置的计算过程中,用到了大量表达式,例如画头发中调用line()函数时的最后一个参数
“faceY - (HairOffsetY + HairLength)”就是一个表达式,在这些表达式中,用到了运算符, 包括进行加减乘除四则运算的算数运算符“+-*/”,以及已经比较熟悉的赋值运算符“=”。
运算符的作用就是将变量和常量组织为表达式,进行特定的运算。
下面再举例一些运算符的用法:
var a = 10; // 1 为变量a赋值10
var b = a*2; // 2 计算a*2的值,然后赋值给变量b
var c = a-b/3; // 3 先计算b/3, 然后计算a-b/3,结果赋值给变量c
var d = (a-b)/3; // 4 先计算a-b,然后除以3,结果复制给变量d
从上述示例3和4可见,当多个运算符组成一个表达式时,它们的计算过程是有顺序的,这就是运算符的优先级问题。例如在3中,最先作用的运算符是/,然后是-,最后是=;而在4中,最先作用的是-,然后是/,最后是=。
去搜索“js运算符优先级”,可以找到很多文章列出所有运算符的优先级,例如:
大家看到这么复杂的表达,内心应该是崩溃的。。。
这里也给出了一个完美解决运算符优先级问题的办法,用括号指定计算顺序!
例如下面一个复杂运算:
var result = (k*m + (p/x))/((a+b)*(c-d)-m*8);
只要从最里面的括号开始分析,就可以看出它的计算顺序:
有关运算符的详细信息,可以查看:http://www.runoob.com/js/js-operators.html
知识点
函数及参数:http://www.runoob.com/js/js-functions.html
运算符:http://www.runoob.com/js/js-operators.html
相关资源
教程示例程序:https://github.com/magicbrush/DrawingByCodingTutorialDemos/