前言
简书没有目录系统,所以。。。对长博文不是很友好。
先声明,这个作业画的图,是临摹的我很喜欢的一个画师的作品,画师名字暂时记不得了,是推特上看到的图。我只是用它来当我的头像了,以及临摹它来交作业,不涉商,应该不会有版权问题。
效果图
先看看原图。原图是全身像,这里只截了头部来画。全画没时间。
然后是代码画的图。给他加了一个动态的背景,模仿的是星轨图。截出来的gif太大了,31MB,传不上来。就只能传个缩略版的了。
鼠标点击之后背景运动会产生变化,按QWS都有一些不同的效果,这个之后再说。鼠标点击的同时还会闭上一只眼睛~
代码实现
画猫猫部分
画这只猫猫的过程,全程只使用了直线和贝塞尔曲线这两种图形,一个点一个点地画出来。最后画了610多行代码,画得身心俱疲,还特别费眼睛。
这当然不可能凭空画,不然画一条线就得反复运行N次,要画得这么写实得画一年。能这么画主要归功于我从网上下的一个小工具(看B站的processing视频,然后关注了某个公众号,在它提供的云盘里下载的),它能把图片作为背景,然后在它上面拖动贝塞尔曲线,按P键就可以输出这条曲线对应的代码片段。
这个代码片段是以beginShape()开头,endShape()结尾的,在这之上可以调整填充色彩和线宽、线色等东西,还是很方便的。
结合图示也很好理解代码的本质含义:
vertex()中的点就是贝塞尔曲线的起始点,bezierVertex()中有3个点坐标,分别是第一控制点、第二控制点和终点。想要画出闭合的贝塞尔曲线,就可以手动调整最后一个点坐标,使之与起始点坐标相同。
如果在这段代码前加上填充效果,那么它会自动把起始点和终点连一条隐性的直线,再把围成的区域填充给色彩。之所以说是隐性直线,是因为它不会被画出来,也就是不会受到线宽、线色等设置的影响。只是填充时可以看出来是一条直线的分割线。
最后,由于代码绘画会完全覆盖的特性,需要仔细安排各个层次绘画的顺序。也可以利用这个特性,在不显示的地方随便画画,显示出来的地方仔细勾勒即可。
星轨背景
其实是网上找的代码改的。源代码在这里,直接复制粘贴就能跑。
要想实现轨迹效果,则需要清屏的时候(这里是画一个大正方形盖上去),填颜色的时候,输入四个参数,即fill(25,25,25,25)这样子,前三个参数是颜色值,这里没有用纯黑,不然我胡子就白画了。第四个参数表示透明度,擦去的时候就不会一下全擦掉,而是擦去浅浅一层,这样轨迹就出现了。
原来的效果是一圈粒子环绕着鼠标旋转并跟随,点击之后会收缩,滚轮控制半径大小。搬到我这里之后就直接定死坐标,就一直绕那个点转了。当然仍然保留了跟随鼠标转的功能,按Q实现。
代码如下:
int num=4000;
PVector v[]=new PVector[num];
color c[] = new color[num];//color of each point.
color ct[]=new color[num];
float theta[] = new float[num];//original angle of each point.
float mtheta[] = new float[num];//translate angle to math value.
float dtheta[] = new float[num];//speed of theta.
float easing[] = new float[num];
int rdt[] = new int[num];//make a shuffle of radius.
float mts = PI/24;//max theta speed.
int r = 1000;//radius of the circle
int rdtr = 0;//range of the rdt
int rdu = 3;//radius of circle
//星轨的主体代码,在draw函数里
for (int i = 0; i<num-1; i++) {
mtheta[i] += dtheta[i];
v[i].lerp(Xi+cos(mtheta[i])*(rdt[i]+r), Yi+sin(mtheta[i])*(rdt[i]+r), 0, easing[i]);
float distance=dist(v[i].x, v[i].y, Xi, Yi);
int intdis=(int)distance+1;
xianzhi=(r/2+100)/intdis;
if (distance<80) {
c[i]=color(random(100, 200), random(100, 200), 255);
if (jishu>=xianzhi) {
fill(c[i]);
ellipse(v[i].x, v[i].y, rdu, rdu);
jishu=0;
}
jishu++;
} else {
c[i]=ct[i];
if (jishu>=xianzhi) {
fill(c[i]);
ellipse(v[i].x, v[i].y, rdu, rdu);
jishu=0;
}
jishu++;
}
}
用到了一个lerp方法,经过大佬的指点,对于坐标点来说,它每次都把这个点坐标向第一、二、三参数所指定的点坐标移动一定距离,这个距离由第四个参数决定,含义是百分比,范围为0~1。
所以,代码里调用lerp方法的第一二参数指定的其实是个半径很大的圆,也就是r指定的圆。圆心设置就在这里。由于二维,所以不需要第三参数,设成0。做三维的话,倒是可以试试给第三个参数加值,做成一个球状烟花。第四个参数就同时控制了轨道稳定时一个点的运动半径和运动速度。一般来说,至少结果上来看,运动速度和半径负相关,速度越快,半径越大。
第四个参数很不好修改。代码决定将第四个参数由随机数生成,范围在0.002~0.008之间,这之间效果不错。再大就太快了,没有运动感,而是瞬移的感觉。再小就太慢了。
另外,这样设置之后,在圆心附近会聚集大量的点,代码里利用反比例函数来限制了点数量:必须达到阈值才能绘制点,这个阈值与点到圆心的距离成反比。当然,系数要精心选择一下。同时还限定了一下颜色,视觉效果更好一点。
当然这样的后果就是,开启移动鼠标模式之后,鼠标周围将出现一段真空地带,将原有的点擦去;移动过快时,原来被鼠标擦去的那些点会突然爆发,对CPU负担可能不小。
交互设计
本来是只想画一幅画就结束的,可是老师要求必须要实现交互。我又不想破坏画好的图,就把心思放在了背景上。实际上,背景也是为了交互而强行添加的,不在原计划范围内。
交互没有采取眼珠移动的方式,因为画的是侧脸,透视关系把不准。就直接选择了鼠标单击-闭眼的表现方法,效果有点。。。嗯。。。滑稽= =。
单击之后,背景会散开。这是因为循环时角度不再递增,点移动的目标点固定了,就会走直线,直到位置达到目标点。这个也可以用键盘的W实现。
按Q实现跟随鼠标点,按S实现不清屏,滚轮控制半径大小。
代码:
void mouseClicked() {
eyeOpen=!eyeOpen;
rot=!rot;
}
void mouseWheel(MouseEvent event) {
float e = event.getCount();
if (e == -1) r+=10;
if (e == 1) r-=10;
}
void keyPressed() {
if (key == 's'||key == 'S') {
drawrec =!drawrec;
}
if(key=='W'||key=='w'){
rot=!rot;
}
if(key=='Q'||key=='q'){
mous=!mous;
}
}
控制布尔变量的真假,然后根据真假再在绘画时选择不同的分支绘画即可。其中,鼠标点击函数对鼠标左中右三个键都会生效。