1.7 开始第一幅“码绘”——用时间控制变量,让懵逼脸动起来

引言——码绘的缺点和优点

在之前的程序中,所有懵逼脸的表情都是僵住不动的。

如此一来,相比于传统纸面绘画,码绘技术还体现不出什么优点,可以说还有一系列缺点:

  • 编写代码麻烦容易出现笔误,
  • 学习编程语言代价高,
  • 不够直观,
  • 缺乏操纵画笔时的控制感和愉悦感,
  • 要用完全逻辑的方式来考虑作画过程,抒情性、即兴性和表意性显得不足。

但是,用编程来作画,可以实现纸面绘画做不出的效果,主要就是两种:动态和交互

在之前的程序中,交互性已经略有体现,我们已经实现了让懵逼脸可以跟随鼠标移动。

然而由于缺乏动态,表情僵硬,还很难说有”码绘“什么特别的优势。

这一节,我们将让懵逼脸动起来,它不仅可以跟随鼠标移动,自身的尺寸/表情/五官位置还绘随时间而发生变化。


一个简单的动画程序案例

先看一个极简的程序例:

  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);
}
它的运行效果是一个随时间缓慢增大的圆形:

1.7 开始第一幅“码绘”——用时间控制变量,让懵逼脸动起来


代码也示意了简单的动画技术,其要点有二:

  • 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);
}

这样便实现了移动的懵逼脸,效果如下:
1.7 开始第一幅“码绘”——用时间控制变量,让懵逼脸动起来


在上述代码中,使用了millis()函数来获取时间,单位为毫秒,然后再通过除法运算转化为秒数,这种方式相比直接调用second()而言,可以获得精度更高的时间值。而用second()函数只能获得整数的读秒计数。
加粗的代码便是实现动态效果的关键。对于posX,首先通过语句“varposX=Second*80;"通过语句让posX的数值正比于秒数Second, 于是,在不同时刻,这几计算出的posX的实际数值也不同;然后,为了让懵逼脸始终位于画布内,用了求余数的算符 %, 让posX对画布宽度width求余数(width是p5.js提供的变量,其数值就是画布宽度:https://p5js.org/reference/#/p5/width )。算符%的作用是求余数,其用法如下列代码:

 1
 2

var a = 5%3; // 对5求3的余数,结果为2,并赋值给a
var b = 9%a; // 对9求a的余数,结果为1, 并赋值给b

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);
}

这样就实现了让懵逼脸的五官随时间发生变化:
1.7 开始第一幅“码绘”——用时间控制变量,让懵逼脸动起来

上述代码中,在调用函数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;
再这几行代码中,实际上是自行设计了一组随时间变化的变量,它们的随时间变化方式,就是组合一些简单的算数运算符而实现的。这里所有的公式都不是基于某种”定律“之类的,而完全是运用数学知识设计并调整出来的。由此可见,掌握一定的基础数学,对于实现丰富效果会有明显帮助的,工欲善其事必先利其器,数学知识也成为了我们进行”码绘“创作需要学习的一门工艺,本教程也将在后续章节中专门对一些常用的数学知识进行讲解和运用。



相关资源