![v2-caf4563b1cb394fe9e66cb328bc23495_1440w.jpg?source=172ae18b](http://img-01.proxy.5ce.com/view/image?&type=2&guid=c4f5753a-2b30-eb11-8da9-e4434bdf6706&url=https://pic1.zhimg.com/v2-caf4563b1cb394fe9e66cb328bc23495_1440w.jpg?source=172ae18b)
之前的圣诞节我们公众号的一篇文章,感觉挺有趣的,现在搬运上来~
一周前我们介绍了大佬 Jason Labbe的一些作品,在“你最想兜兜鱼拆解的作品”投票活动中,Noise flow field painter最终胜出。
在这个圣诞夜,我们特别挑选了他的作品源代码进行拆解,来教你如何做个火遍朋友圈的圣诞老人~
![v2-5038d8212ae2af79865e4e9942b62e9a_b.jpg](http://img-01.proxy.5ce.com/view/image?&type=2&guid=c4f5753a-2b30-eb11-8da9-e4434bdf6706&url=https://pic3.zhimg.com/v2-5038d8212ae2af79865e4e9942b62e9a_b.jpg)
这位大佬写过许多艺术化作品。有兴趣的同学可以去他的个人网站看看,这里特别感谢大佬授权我们使用他的作品~
![v2-98b606f59554621f6b422dcd5c1ee4c1_b.gif](http://img-02.proxy.5ce.com/view/image?&type=2&guid=c4f5753a-2b30-eb11-8da9-e4434bdf6706&url=https://pic2.zhimg.com/v2-98b606f59554621f6b422dcd5c1ee4c1_b.gif)
代码地址:https://www.openprocessing.org/sketch/472966
![v2-b2e992e309037f8d2e9b4533470049c5_b.png](http://img-01.proxy.5ce.com/view/image?&type=2&guid=c4f5753a-2b30-eb11-8da9-e4434bdf6706&url=https://pic2.zhimg.com/v2-b2e992e309037f8d2e9b4533470049c5_b.png)
先放点帅气的作品照片:
![v2-61e21ee73dcb22f1a3f56ed66b04322c_b.jpg](http://img-01.proxy.5ce.com/view/image?&type=2&guid=c4f5753a-2b30-eb11-8da9-e4434bdf6706&url=https://pic1.zhimg.com/v2-61e21ee73dcb22f1a3f56ed66b04322c_b.jpg)
![v2-a535d78e4ca501eb60830eb65fbb01cf_b.gif](http://img-03.proxy.5ce.com/view/image?&type=2&guid=c4f5753a-2b30-eb11-8da9-e4434bdf6706&url=https://pic4.zhimg.com/v2-a535d78e4ca501eb60830eb65fbb01cf_b.gif)
![v2-4ff48649035ac9b411fb15e3b7c5aed7_b.jpg](http://img-01.proxy.5ce.com/view/image?&type=2&guid=c4f5753a-2b30-eb11-8da9-e4434bdf6706&url=https://pic4.zhimg.com/v2-4ff48649035ac9b411fb15e3b7c5aed7_b.jpg)
![v2-b5774f1196895534788004ff59a38bd8_b.gif](http://img-03.proxy.5ce.com/view/image?&type=2&guid=c4f5753a-2b30-eb11-8da9-e4434bdf6706&url=https://pic1.zhimg.com/v2-b5774f1196895534788004ff59a38bd8_b.gif)
![v2-a8cc7ae1af6c1ffffe44c066598f0f0f_b.jpg](http://img-01.proxy.5ce.com/view/image?&type=2&guid=c4f5753a-2b30-eb11-8da9-e4434bdf6706&url=https://pic4.zhimg.com/v2-a8cc7ae1af6c1ffffe44c066598f0f0f_b.jpg)
![v2-1154fc49d4d050c147d36329c803ee6c_b.gif](http://img-02.proxy.5ce.com/view/image?&type=2&guid=c4f5753a-2b30-eb11-8da9-e4434bdf6706&url=https://pic1.zhimg.com/v2-1154fc49d4d050c147d36329c803ee6c_b.gif)
拆解注释:
int maxTime = 400;
int strokesPerFrame = 25; 全帧动画为24帧,这里strokePerFrame用于循环。
//把图片加载到图片数组
//对不超过700像素的图片效果最好
String[] imgNames = {"emma.jpg", "obama.jpg", "clint.jpg"};
PImage img;
int imgIndex = -1;
float brightnessShift;
void setup() {
size(950, 700);
colorMode(HSB, 255); //调整色彩模式为HSB模式。(H(hues)色相,S(saturation)饱和度,B(brightness)亮度)
nextImage();
}
void draw() {
translate(width/2, height/2);
for (int i = 0; i < strokesPerFrame; i++) { //小于strokePerFrame,到24为止
/*画面中像素总颗粒数=width*height,由于之后要读取画布上每一个像素值,
这里先声明一个随机的像素下标。用于选取画面中的像素*/
int index = int(random(img.width*img.height));
//在画面中读取第index个像素对应的颜色
color pixelColor = img.pixels[index];
pixelColor = color(red(pixelColor), green(pixelColor), blue(pixelColor), 255);
//将颜色重新组合,将图片的RGB通道用于HSB模式。
int x = index%img.width;
int y = index/img.width;
pushMatrix();
translate(x-img.width/2, y-img.height/2);
if (frameCount % 5 == 0) {
paintDot(pixelColor, (int)random(2, 20)*map(frameCount, 0, maxTime, 1, 0.5));
} else {
paintStroke(random(150, 250), pixelColor, (int)random(2, 8)*map(frameCount, 0, maxTime, 1, 0.1), map(frameCount, 0, maxTime, 40, 5));
}
popMatrix();
}
if (frameCount > maxTime) { //当过了一定的时间,就停止循环。
noLoop();
}
}
void mousePressed() { //鼠标点击切换图片
nextImage();
}
void nextImage() { //切换图片
background(255);
//由于draw里没有使用background(),画面是在不断叠加的,更换图片的时候需要清空一次画面重新开始生成画面。
loop();
//由于刚刚画面生成以后停止了循环,因此更换图片的时候需要重新开启循环。
frameCount = 0;
//并且把processing内部记帧数变量frameCount调整为0
brightnessShift = random(255);
//随机产生亮度的变动值
imgIndex++; //图片下标更改
if (imgIndex >= imgNames.length) { //超出图片数组边界就回到第一张图片,因此完成循环读取图片
imgIndex = 0;
}
img = loadImage(imgNames[imgIndex]);//读取图片
img.loadPixels();//分解像素
}
//画线函数
void paintStroke(float strokeLength, color strokeColor, int strokeThickness, float length) {
float b = brightness(strokeColor);//亮度的获取,由于图片是黑白照片,因此图片不同亮度的像素点排列可用以识别图片。
float bShift = b+brightnessShift; //亮度的渐变
if (bShift > 255) {
bShift -= 255;
}
pushMatrix();
rotate(radians(map(b, 0, 255, -180, 180))); //根据亮度值来旋转坐标
//沿着x轴上中下位置画出了三条不同亮度的直线,线条的b值(亮度)是随着亮度值的变化而变化。
stroke(map(bShift, 0, 255, 0, 255), 150, map(b, 0, 255, 0, 100), 50); //根据亮度以及亮度的变化来设置描边颜色。
line(-length, 1, length, 1);
stroke(map(bShift, 0, 255, 0, 255), 150, map(b, 0, 255, 0, 255));
strokeWeight(strokeThickness); //中间的线条粗度是在一定范围内获取的,但这个范围会随着循环的进行变小。
line(-length, 0, length, 0);
stroke(map(bShift, 0, 255, 0, 255), 150, map(b, 0, 255, 150, 255), 20);
line(-length, 2, length, 2);
popMatrix();
}
//画点函数(实际上还是画线,但线条调整得粗短了一些,看起来像是长条的点)
void paintDot(color strokeColor, int strokeThickness) {
//亮度值的变化和回调
float b = brightness(strokeColor);
float bShift = b+brightnessShift;
if (bShift > 255) {
bShift -= 255;
}
pushMatrix();
rotate(radians(random(-180, 180)));//点的旋转角度是随机的
stroke(map(bShift, 0, 255, 0, 255), 150, map(b, 0, 255, 0, 255));
strokeWeight(strokeThickness);
line(0, 0, 5, 0);
popMatrix();
}
这次我会从粗略放到细致,以整体看到局部的方式来解剖这个例子。因此首先,我们需要探究的问题是:这样的作品,整体上在做什?这是解剖的第一个层级,我用这样的方式呈现给大家:
//声明需要的全局变量
void setup(){
//初始化数据
}
void draw(){
//根据载入的图片选取颜色
//根据帧数的累计选择画点或画线
}
void nextImage(){
//切换图片
}
void paintStroke(){
//画线函数
}
void paintDot(){
//画点函数
}
这样子看就清晰了很多,这个例子的最终目的就是用从图像获取颜色后的点后和线,来画出原图中的人像。
当我们知道了做什么,那就要考虑怎么做的问题。怎么做大致可分成四部分:
1. 怎么画点
2. 怎么画线
3. 什么时候画点或线(draw)
4. 切换图片
前三个关联性比较强,且draw中做的事情是包含了前两个独立定义的函数的。
所以解剖代码的第二个层级,就是draw中发生的事情。
int strokesPerFrame = 25;
void draw() {
translate(width/2, height/2);
for (int i = 0; i < strokesPerFrame; i++) {
//在屏幕上随机选择一个像素点来获取颜色
int index = int(random(img.width*img.height));
color pixelColor = img.pixels[index];
pixelColor = color(red(pixelColor), green(pixelColor), blue(pixelColor), 255);
//位置的调整
int x = index%img.width;
int y = index/img.width;
pushMatrix();
translate(x-img.width/2, y-img.height/2);
//画点还是画线的判断
if (frameCount % 5 == 0) {
paintDot(pixelColor, (int)random(2, 20)*map(frameCount, 0, maxTime, 1, 0.5));
} else {
paintStroke(random(150, 250), pixelColor, (int)random(2, 8)*map(frameCount, 0, maxTime, 1, 0.1), map(frameCount, 0, maxTime, 40, 5));
}
popMatrix();
}
if (frameCount > maxTime) {
noLoop();
}
}
draw中的内容主要分成了三个部分:取色,选位置,画图。
首先取色部分很简单,简单地在所有像素点上随机取一个点,简单地用图片读取读取那一个点位置上的像素颜色,简单地设置好颜色。
int index = int(random(img.width*img.height));
color pixelColor = img.pixels[index];
pixelColor = color(red(pixelColor), green(pixelColor), blue(pixelColor), 255);
接着是选位置,计算出了每次画点或者画线的位置。
int x=index%img.width;
int y=index/img.width;
x的位置数据为随机的一个像素点对图片宽度求余得到的数字,而y的位置数据为随机的一个像素点除以宽度得到的数字。
由于在draw的一开始坐标位置移动到了中心位置,如果直接开始画图像,图像会从中心点开始画,这里的因此这里translate中计算好的x,y位置会需要减去宽/高 的一半,这样的图像的中心位置就会出现在中心了。
translate(x-img.width/2,y-img.height/2);
接着进行画点或者画点的判定,if (frameCount % 5 == 0) 这个是判定条件。
frameCount是对draw循环次数的累计,让这个数除以5以后求余数,如果等于零代表能被5除尽,就采用画点的函数,不然就采用画线的函数。
这样可以让两个画图函数随着frameCount 的增加而分别出现。最后我们对画图的时间进行限制就可以了。
if (frameCount>maxTime) {
noLoop();
}
当frameCount的计算大于设定的最大时间的时候,就停止loop,调用noLoop()函数就可以让draw()停止循环。
draw函数一般是整合所有元素的函数,搞清楚了draw在做什么,就可以宏观地知道整个程序在做什么了,之后再来微观地看细则。主要是两个画图函数paintDot(); 和 paintLine();
进入解剖的第三个层级:
void paintStroke(float strokeLength, color strokeColor, int strokeThickness, float length) {
float b = brightness(strokeColor);
float bShift = b+brightnessShift;
if (bShift > 255) {
bShift -= 255;
}
pushMatrix();
rotate(radians(map(b, 0, 255, -180, 180)));
stroke(map(bShift, 0, 255, 0, 255), 150, map(b, 0, 255, 0, 100), 50);
line(-length, 1, length, 1);
stroke(map(bShift, 0, 255, 0, 255), 150, map(b, 0, 255, 0, 255));
strokeWeight(strokeThickness);
line(-length, 0, length, 0);
stroke(map(bShift, 0, 255, 0, 255), 150, map(b, 0, 255, 150, 255), 20);
line(-length, 2, length, 2);
popMatrix();
}
这个函数会需要填入四个参数,(描边长度(这个似乎没用到?),描边颜色,描边粗度,长度)
float b = brightness(strokeColor);
float bShift = b+brightnessShift;
if (bShift > 255) {
bShift -= 255;
}
brightness可以获取颜色变量的亮度参数,brightnessShift为在最开始声明的存储亮度变化的变量,bShift存储的则是亮度在变化后的数值,当bShift超过255之后,颜色又会跳回0,重新开始增加。
而后半部分就是画出三条线的意思了,线条的颜色是根据bShift的变化映射而来的。分别将映射后的值填入线条的描边色中,但每个线条的映射范围都不仅相同,随着bShift的变化,每次循环画出的线条色彩都有一定的差异。并且你会观察到:在画线条之前,代码中还会将坐标角度进行一定的旋转。
rotate(radians(map(b, 0, 255, -180, 180)));
旋转的角度是由亮度值映射而来的,从0到255的亮度分别对应着-180到180的旋转角度,这样可以让线条看起来更自然。如果去除这一句,线条都是同一方向的。
最后调用画线函数line(这里填入长度参数);
![v2-851c8a10d1614bc8d608cf7ea91ae34f_b.gif](http://img-01.proxy.5ce.com/view/image?&type=2&guid=c4f5753a-2b30-eb11-8da9-e4434bdf6706&url=https://pic4.zhimg.com/v2-851c8a10d1614bc8d608cf7ea91ae34f_b.gif)
![v2-69e185eb4ae7cc45e4c94a16ddf9061a_b.gif](http://img-01.proxy.5ce.com/view/image?&type=2&guid=c4f5753a-2b30-eb11-8da9-e4434bdf6706&url=https://pic3.zhimg.com/v2-69e185eb4ae7cc45e4c94a16ddf9061a_b.gif)
2.paintDot();
void paintDot(color strokeColor, int strokeThickness) {
float b = brightness(strokeColor);
float bShift = b+brightnessShift;
if (bShift > 255) {
bShift -= 255;
}
pushMatrix();
rotate(radians(random(-180, 180)));
stroke(map(bShift, 0, 255, 0, 255), 150, map(b, 0, 255, 0, 255));
strokeWeight(strokeThickness);
line(0, 0, 5, 0);
popMatrix();
}
paintDot需要给的参数有,描边颜色和描边粗度,与paintStroke类似,上半部分是对用于颜色映射的变量bShift的操控,后半部分同样是坐标的旋转,颜色的映射和画线,但这里有对线的粗度进行调整,并且把线条的长度值调小了。这与原来的线条相比,看起来更像是一个点。
![v2-462fd9f7e9d4d1505dadc0951024f34f_b.gif](http://img-02.proxy.5ce.com/view/image?&type=2&guid=c4f5753a-2b30-eb11-8da9-e4434bdf6706&url=https://pic4.zhimg.com/v2-462fd9f7e9d4d1505dadc0951024f34f_b.gif)
函数调用
再看到draw里面调用这两个函数的时候填入的是什么参数呢?
函数定义:
void paintStroke(float strokeLength, color strokeColor, int strokeThickness, float length)
函数调用:
paintStroke(random(150, 250),
pixelColor,
(int)random(2, 8)*map(frameCount, 0, maxTime, 1, 0.1),
map(frameCount, 0, maxTime, 40, 5));
可以看到粗度和长度都是根据frameCount映射而来的,数值会根据时间的进行而越变变小,由于整个画面在循环时并没有在每一次循环用background来清除屏幕。因此整个画面是覆盖而成的,而笔画的变化会给画面添加一个由底部到顶部由粗犷变到细致的过程。
函数定义:
void paintDot(color strokeColor, int strokeThickness)
函数调用:
paintDot(pixelColor,
(int)random(2, 20)*map(frameCount, 0, maxTime, 1, 0.5));
相同原理的画点函数,同样也是用映射来调整粗度。
交互部分:
void setup(){
nextImage();
}
void mousePressed() {
nextImage();
}
void nextImage() {
background(255);
loop();
frameCount = 0;
brightnessShift = random(255);
imgIndex++;
if (imgIndex >= imgNames.length) {
imgIndex = 0;
}
img = loadImage(imgNames[imgIndex]);
img.loadPixels();
}
这个例子的交互部分很简答,单击切换一张图片来生成效果。全部都是在nextImage()中完成的。
首先因为要切换图片,background的出现就是为了清除之前的画面,否则新的图片会覆盖在之前的画面上。在draw里有当一张图片生成完成就noLoop停止循环的设定,因此在生成新图片时要重新开启循环loop。
而frameCount映射了线条的粗度和长度,在frameCount为0的时候映射的数值是最大的,然后逐渐变小,因此frameCount需要调整回0重新增长。
frameCount与线条的形状有关,而brightnessShift则和线条的颜色相关,线条颜色是由bShift映射的,而使得bShift变化的参数是brightnessShift和b,因此根源上线条颜色和后两者相关,所以在切换图片的时候,也需要把这两个参数重新做调整。
brightnessShift是在 (0,255) 随机选择了一个值,而b值,则是通过重新加载图片之后,重新读取图片像素来重新加载b值的。因为b值是根据 strokeColor得来的。
float b = brightness(strokeColor);
strokeColor是一个形式参数,实际在调用的时候填入的是pixelColor,是会在draw里对图像获取的某一个像素点的颜色。
![v2-b01608ddfdc64c05e7a674708bd28812_b.jpg](http://img-03.proxy.5ce.com/view/image?&type=2&guid=c4f5753a-2b30-eb11-8da9-e4434bdf6706&url=https://pic3.zhimg.com/v2-b01608ddfdc64c05e7a674708bd28812_b.jpg)
所以在nextImage中,切换图片之后需要重新读取所有图片的像素点。
img = loadImage(imgNames[imgIndex]);
img.loadPixels();
imgIndex指的是第几张图片。这里总共三张,imgNames数组里存储了三个用来家在的图片的名字,所以图片数量也是imgNames这个字符串数组的长度。当图片下标超出的时候,就让下标回到零。图片就会随着鼠标的单击循环切换了。
imgIndex++;
if (imgIndex >= imgNames.length) {
imgIndex = 0;
}
![v2-b44bbe2e42e7e59036dfc603cd270ce1_b.gif](http://img-03.proxy.5ce.com/view/image?&type=2&guid=c4f5753a-2b30-eb11-8da9-e4434bdf6706&url=https://pic2.zhimg.com/v2-b44bbe2e42e7e59036dfc603cd270ce1_b.gif)
最后献上生成的完整的圣诞老人,祝大家圣诞快乐~
![v2-355a8c36656589e48d5ee85d14bba92e_b.jpg](http://img-03.proxy.5ce.com/view/image?&type=2&guid=c4f5753a-2b30-eb11-8da9-e4434bdf6706&url=https://pic3.zhimg.com/v2-355a8c36656589e48d5ee85d14bba92e_b.jpg)