引言——码绘的缺点和优点
在之前的程序中,所有懵逼脸的表情都是僵住不动的。
如此一来,相比于传统纸面绘画,码绘技术还体现不出什么优点,可以说还有一系列缺点:
- 编写代码麻烦容易出现笔误,
- 学习编程语言代价高,
- 不够直观,
- 缺乏操纵画笔时的控制感和愉悦感,
- 要用完全逻辑的方式来考虑作画过程,抒情性、即兴性和表意性显得不足。
但是,用编程来作画,可以实现纸面绘画做不出的效果,主要就是两种:动态和交互。
在之前的程序中,交互性已经略有体现,我们已经实现了让懵逼脸可以跟随鼠标移动。
然而由于缺乏动态,表情僵硬,还很难说有”码绘“什么特别的优势。
这一节,我们将让懵逼脸动起来,它不仅可以跟随鼠标移动,自身的尺寸/表情/五官位置还绘随时间而发生变化。
一个简单的动画程序案例
先看一个极简的程序例:
1 2 3 4 5 6 7 8 9 10 11 |
function
setup
()
{
createCanvas
(
640
,
480
);
}
function
draw
()
{
// 调用函数second(),获得程序运行经过的秒数
var
s
=
second
();
// 在屏幕中心画圆圈,长宽等于秒数
ellipse
(
320
,
240
,
s
,
s
);
}
|
代码也示意了简单的动画技术,其要点有二:
- draw()函数随时间反复执行;
- 每一次执行draw()函数,通过调用函数second()获得程序当前经过的秒数,并且在绘制圆形时用获得的时间数值来指定圆形的尺寸。
制造动态的基本方法
从上述简单的动画示例,可以导出制作动态的基本方法,写成一个函数如下:
1 2 3 4 5 |
function
LoopFunction
()// 这个函数应该被框架程序持续反复调用
{
UpdateEveryThing
();
// 更新每一个东西
DrawEveryThing
();
// 绘制每一个东西
}
|
可见,有两个要领:1.要定义一个在程序框架中被持续反复调用的函数;2.在该函数中,要更新并绘制一些东西;
再对应到p5.js提供的程序框架,可见其draw()函数符合上述要求,只要能够在draw()函数中更新需要绘制物体的信息并将它们绘制出来,就能产生动画效果。
让懵逼脸移动起来
利用之前写出的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 |
// 画懵逼脸
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 画头发 ---------------
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
)
);
}
|
我们重新写一个简单的draw()函数,实现一个持续移动的懵逼脸:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// 函数draw():作画阶段
function
draw
()
{
// -------- 获取时间 ------------------- //
// 获得毫秒数
var
Millis
=
millis
();
// 获得秒数,相比second(),能获得小数点后的部分
var
Second
=
millis
()
/
1000
;
// -------- 计算位置 --------------------//
var posX = Second * 80;
posX = posX%width; // 让posX不超过屏幕宽度
var posY = Second * 61;
posY = posY%height; // 让posY不超过屏幕高度
// --------- 绘制懵逼脸------------------//
drawConfuseFace
(
posX
,
posY
,
80
,
0.3
,
0.2
,
0.2
);
}
|
这样便实现了移动的懵逼脸,效果如下:
在上述代码中,使用了millis()函数来获取时间,单位为毫秒,然后再通过除法运算转化为秒数,这种方式相比直接调用second()而言,可以获得精度更高的时间值。而用second()函数只能获得整数的读秒计数。
加粗的代码便是实现动态效果的关键。对于posX,首先通过语句“varposX=Second*80;"通过语句让posX的数值正比于秒数Second, 于是,在不同时刻,这几计算出的posX的实际数值也不同;然后,为了让懵逼脸始终位于画布内,用了求余数的算符 %, 让posX对画布宽度width求余数(width是p5.js提供的变量,其数值就是画布宽度:https://p5js.org/reference/#/p5/width )。算符%的作用是求余数,其用法如下列代码:
posY的计算方式于posX完全一样。
于是,在最后调用drawConfuseFace()函数时,由于用随时间发生变化的量posX和posY作为位置,因此画出来的懵逼脸便随着时间移动了。
让懵逼脸的表情动起来
通过随时间发生变化的变量来控制位置,便实现了懵逼脸的移动。
同理,若构造出能够随时间变化的变量来控制懵逼脸的尺寸,便可以实现懵逼脸的表情动画效果。于是,我们尝试将代码进一步改造如下:
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 |
// 函数draw():作画阶段
function
draw
()
{
// -------- 获取时间 ------------------- //
// 获得毫秒数
var
Millis
=
millis
();
// 获得秒数,相比second(),能获得小数点后的部分
var
Second
=
millis
()
/
1000
;
// -------- 计算位置 --------------------//
var
posX
=
Second
*
80
;
posX
=
posX
%
width
;
// 让posX不超过屏幕宽度
var
posY
=
Second
*
61
;
posY
=
posY
%
height
;
// 让posY不超过屏幕高度
// -------- 计算懵逼脸五官尺寸 ----------//
var Second20 = Second * 20;
var FaceSize = (Second20 + 50)%100;
var MouseScale = ((Second20 + 30)%50 )/100;
var LEyeScale = (((Second20 + 20)%40)/120 ) + 0.1;
var REyeScale = 0.3 - ((Second20 + 20)%40)/150;
// --------- 绘制懵逼脸------------------//
drawConfuseFace
(
posX,posY,
FaceSize,
MouseScale,
LEyeScale,
REyeScale);
}
|
这样就实现了让懵逼脸的五官随时间发生变化:
上述代码中,在调用函数drawConfuseFace()时,所有的参数都用随时间变化的变量来指定,于是,每一次调用draw()函数时,由于这些变量的数值都不同,这就实现了在每一次绘制懵逼脸时让五官的位置和尺寸都不同。
这里再截取出关键代码:
// -------- 计算懵逼脸五官尺寸 ----------//
var Second20 = Second * 20;
var FaceSize = (Second20 + 50)%100;
var MouseScale = ((Second20 + 30)%50 )/100;
var LEyeScale = (((Second20 + 20)%40)/120 ) + 0.1;
var REyeScale = 0.3 - ((Second20 + 20)%40)/150;