博文索引目录:
1. 引言
2. 临摹结果对比
3. 临摹过程
- 3.1 准备工作
- 3.2 原图规律——语言描述
- 3.3 原图规律——数学与代码描述
- 3.4 完整代码
4. 创意拓展
- 4.1 拓展一——时空穿梭,一切随机
- 4.2 拓展二——炫彩光影,交互控制
5. 总结
1. 引言
大三上学期开设了一门非常有趣的课程——互动媒体技术,主要是用代码来“画画”。目前学习的阶段处于临摹与拓展并学习了p5.js,本次选取了一个比较好看的GIF光点图进行了临摹和拓展,本篇博文将对本次临摹绘制与对原图进行拓展的过程进行详细论述,使用的是p5.js来码绘。下面就跟着我进入炫彩的光影世界吧!
2. 临摹结果对比
首先摆出原图与我的临摹作品的对比效果。
(温馨提示:刚开始打开博文加载GIF图时可能会有点儿卡,需要耐心等待片刻,GIF图就能流畅播放啦~)
- 原图:
- 临摹作品:
可以看到,两幅图的相似度还是蛮高的,我的临摹作品相较于原图来说运动稍微有点儿不流畅,但整体观感影响不大。下面将具体展示我的临摹过程。
3. 临摹过程
- 3.1 准备工作
为了使临摹出来的图形还原度高,本次临摹的主要准备工作在于图像的取色。打开PS,用拾色器可以获取选中的点,里面可以看到RGB值等,需要注意的是,本图的背景色是深灰而不是黑,总之,取与原图相同的颜色很重要,取完颜色我们就创建一个画布,然后设置其背景色,就可以来分析原图中的运动规律了。
创建画布,设置背景色:
function setup()
{
// 创建画布400*400
createCanvas(400,400);
// 整个画布为深灰色
background(38,38,38);
}
另外,上图中PS里的时间轴工具可以一帧一帧播放GIF图,慢速播放能够更快地帮助我们找到规律。
- 3.2 原图规律——语言描述
观察原GIF图,图形主要由基本的元素点和线组成。
—>对于点(其实是圆):
① 均匀分布在画面中,为8×8的方阵。
② 每一列点的颜色相同,且没有发生变化。
③ 每一列的点匀速向下运动,若运动超出画面下边界,则又从上边界出现。
—>对于线:
① 每一时刻画面中心点(由于画面大小为400*400,所以这里中心点就是(200,200))与每个点的连线以后,去掉两点之间的线段部分,就是线的基本形状。
② 每条线的颜色与连接该线的点的颜色相同(所以就好像是该点发出了光线)。
③ 线的粗细由点的位置来决定,点最开始进入画面中时线是细的,然后运动到纵坐标接近画面中央时慢慢加粗,在纵坐标接近画面底部时又慢慢变细。
④ 线都是由点发出的,如果线比较粗的时候,看上去就像一个扇形。
⑤ “光线”具有一定的透明度。
- 3.3 原图规律——数学与代码描述
首先,我们画点的分布,第一帧开始的时候,点均匀分布在画布中,由于是8×8的点阵,画布大小为400×400,所以相邻每行、每列点之间的距离为50,且第一帧左上角的第一个点的坐标经过简单的数学计算可以得出是(25,25),我们可以定义为全局变量:
var xpos=25.0; //初始时左上角第一个点的x坐标
var ypos=25.0; //初始时左上角第一个点的y坐标
根据第一个点的初始坐标就可以计算出其他每个点的初始坐标,我们可以在draw()函数中用两个for循环画出点阵。
//画8*8的点阵
for(i=0;i<8;i++)
for(j=0;j<8;j++)
{
//每一列点的颜色不同
switch(i)
{
case 0: fill(238,33,90); break;
case 1: fill(198,33,132);break;
case 2: fill(99,41,140); break;
case 3: fill(16,115,188);break;
case 4: fill(0,165,148); break;
case 5: fill(132,198,66);break;
case 6: fill(242,179,43);break;
case 7: fill(238,90,49); break;
}
//画点
ellipse(xpos+i*50,ypos+j*50,10,10);
}
这样画出来的点阵图为静态的,如下图所示:
那么怎么让点动起来呢?我们需要用到时间t这个变量,通过millis()函数获取运行的时间,注意这里单位是毫秒,需要除以1000得到的单位才为秒。
//程序运行的时间(单位:秒)
t=millis()/1000;
每一帧渲染的时候都要调用一次draw()函数,所以每一次都要更新圆点的位置,由于每一列原点向下匀速运动,所以与时间有关的变量只有原点的纵坐标。下面代码中78是点向下移动的速度,是经过反复与原图的速度进行比较调试后得到的结果,在原来的静态纵坐标下加上 (t*78)%400 的意思是,每一秒的时间,点就向下移动78个单位,这就实现了点的匀速运动,除以400取余同样是个非常重要的操作,因为如果不取余,那么圆点会随时间增加一直向下运动,这样过一点儿时间以后画面中就没有圆点了,因为它们已经无限地向下运动走了,取余后增加的位移会永远在400以内。
var x=xpos+i*50; //实际画的时候点的x坐标
var y=(t*78)%400+ypos+j*50; //实际画的时候点的y坐标
为了让点运动到画布下界后能够回到上界重新往下运动,需要做一个判断,即如果纵坐标大于画布最大纵坐标400,则要减400。
//如果点运动到画布下界,则从上界开始重新往下运动
if(y>=400)
y-=400;
运行程序以后发现,小球虽然在往下匀速运动,且也会不断循环,但在小球之间的运动轨迹仍然会出现并不断被刷新。
为了解决这个“模糊”的现象,我发现只要在每帧画的开始将画布背景重置一次就能解决。
function draw()
{
//程序运行的时间(单位:秒)
t=millis()/1000;
//每帧都将画布刷为深灰色,不然会出现模糊现象
background(38,38,38);
接下来就是画线了。刚才画圆我们用fill()函数设置了填充颜色,这里画线我们需要用stroke()函数,它采用了RGBA模式,前三个参数还是RGB值,通过第四个参数,我们可以设置其透明度的变化,这样就能实现具有一定透明度的“光线”。从下图代码中可以看到,从中间往两边,透明度在减小。
switch(i)
{
case 0: fill(238,33,90); stroke(238,33,90,100);break;
case 1: fill(198,33,132);stroke(198,33,132,95);break;
case 2: fill(99,41,140); stroke(99,41,140,90);break;
case 3: fill(16,115,188);stroke(16,115,188,40);break;
case 4: fill(0,165,148); stroke(0,165,148,40);break;
case 5: fill(132,198,66);stroke(132,198,66,90); break;
case 6: fill(242,179,43);stroke(242,179,43,95); break;
case 7: fill(238,90,49); stroke(238,90,49,100);break;
}
由之前的规律描述我们知道,每条线都是画布中心点(200,200)与每个点的连线形成的,并且这条线与画布边界是相交的,所以我们需要计算出每条线与画布边界的交点,求交点的过程需要将整个画布区域的点分成几个区域分情况讨论。
我们记每个圆点的坐标为(x0,y0),记每个圆点与画布中心点的直线与画布边界的交点坐标为(linex,liney)
这样可以得到直线方程为:
y-y0=(200-y0)/(200-x0)*(x-x0)
因为点的坐标(x0,y0)我们已知,现在只需知道linex或者liney,我们就能根据方程求出另外一个。而圆点在不同区域的交点坐标又不同,于是我将画布区域分成了六个部分,比如第一块区域交点的纵坐标总是0,第二块区域交点的横坐标总是0,这样便能很快地求出交点坐标了。原理图如下:
我分成了六个区域,实际上有些区域还可以继续合并,形成更少的条件分支(可以看到,比如区域1和5,它们交点的纵坐标都是0,分支里面的代码其实也是一样的,但在这里为了便于理解,分成了六个区域)。代码如下,用line()函数画线:
//找直线与边界的交点坐标
var linex,liney; //交点的x、y坐标
//区域1
if ( x<200 && x>y )
{
linex=(200-x)/(y-200)*y+x;
liney=0;
}
//区域2
else if( x<200 && x<=400-y)
{
linex=0;
liney=(200-y)/(x-200)*x+y;
}
//区域3
else if ( x<200 && x>400-y )
{
linex=(200-x)/(y-200)*(y-400)+x;
liney=400;
}
//区域4
else if(x>200 && x<y)
{
linex=(200-x)/(y-200)*(y-400)+x;
liney=400;
}
//区域5
else if ( x>200 && x<=400-y)
{
linex=(x-200)/(200-y)*y+x;
liney=0;
}
//区域6
else
{
linex=400;
liney=(200-y)/(200-x)*(400-x)+y;
}
line(linex,liney,x,y);
此时效果图截图如下,虽然我们找到了交点,也成功地画出来线了,但线的宽度都是1,没有动态的粗细变化过程:
怎么样实现线的粗细的动态变化呢?首先,我将粗细看成是画线条数的不同,即粗的线是由很多条线组成的,那么线的粗细变化也就是画线条数的变化,这个条数我定义为重复次数,可以从原图的规律中看出,越中心的点重复次数越多,在反复经过与原图的对比测试以后,我找到了一组相近的重复次数数值,如下代码:
var times; //重复次数
switch(i) //每一列的“灯光”粗细不同
{
case 0: times=2;break;
case 1: times=4;break;
case 2: times=6;break;
case 3: times=25;break;
case 4: times=25;break;
case 5: times=6; break;
case 6: times=4; break;
case 7: times=2;break;
}
需要说明的是,这个重复次数其实是当点处于区域2和6的情况下的重复次数,也就是说,当交点的横坐标为0或400的时候,我们对交点纵坐标liney上下各times个单位的点都与圆点坐标(x0,y0)相连线,得到的效果是我们想要的。
//如果与竖直边界相交
if(linex==0 ||linex==400)
{
for (var m = 1; m <times ; m++)
{
line(linex,liney-m,x,y);
line(linex,liney+m,x,y);
}
}
如果这种方式应用到其他区域,也就是与水平边界相交时,会出现太粗与原图不符的情况,出现这种情况的原因是原图的光线在中间部分看起来很粗,几乎没有变化, 而在光线出现或即将消失时,它将很细,所以在与水平边界相交时,我们需要做出粗细渐变的效果,那么我们该怎么做呢?我模拟了一种最为简单的渐变方式——线性变化,调试后发现结果还不错。
以区域3的点的线的线性变化为例,再以那一列蓝色的点为例(即横坐标为175的那一列点),当这些点到达(175,225)的位置时,它们的重复次数times还是25,要想让它们进行线性变化,那么当它们到达(175,400)时重复次数应该为一个我们规定的比较小的值(这里定为2)。于是可以将这个线性过程看成:当点的相对纵坐标从0到x0时(这里x0是175,又因为分界点处夹角为45°),重复次数由times到2的线性减少,于是就有了下图中的线性函数,显然可以得到这个函数方程为:y=(2-times)/x0*x+times
而主要在于求这个x,即在分界线以后开始做线性变化时,纵坐标移动了多少,下面这张图很清晰地表示出了x与y的关系,可以得到:
x=x0-(400-y0)=x0+y0-400
由上述数学原理我们就可以确定重复次数了,在画线时发现如果调整一下次数再除以3,能够比较贴近原图的粗细。
以上是与水平底边相交的情况,与水平顶边相交的情况同理,代码如下:
//如果与水平底边相交
if(liney==400)
{
var times1=(2.0-times)*(x+y-400)/x+times;
for(var n=1; n< times1; n++)
{
line(linex-n/3,liney,x,y);
line(linex+n/3,liney,x,y);
}
}
//如果与水平顶边相交
if(liney==0)
{
var times2=(times-2.0)*y/x+2.0;
for(var k=1; k< times2; k++)
{
line(linex-k/3,liney,x,y);
line(linex+k/3,liney,x,y);
}
}
至此,我们的临摹作品代码部分已经全部完成,运行,可以看到前面所展示的动态图形。
- 3.4 完整代码
完整代码如下,具体讲解已在上文中阐述过,这里仅仅做个代码整合:
// 函数setup() : 准备阶段
function setup()
{
// 创建画布400*400
createCanvas(400,400);
// 整个画布为深灰色
background(38,38,38);
}
var xpos=25.0; //初始时左上角第一个点的x坐标
var ypos=25.0; //初始时左上角第一个点的y坐标
var t; //程序运行时间
// 函数draw():作画阶段
function draw()
{
//程序运行的时间(单位:秒)
t=millis()/1000;
//每帧都将画布刷为深灰色,不然会出现模糊现象
background(38,38,38);
//画8*8的点阵
for(i=0;i<8;i++)
for(j=0;j<8;j++)
{
//每一列点的颜色不同
switch(i)
{
case 0: fill(238,33,90); stroke(238,33,90,100);break;
case 1: fill(198,33,132);stroke(198,33,132,95);break;
case 2: fill(99,41,140); stroke(99,41,140,90);break;
case 3: fill(16,115,188);stroke(16,115,188,40);break;
case 4: fill(0,165,148); stroke(0,165,148,40);break;
case 5: fill(132,198,66);stroke(132,198,66,90); break;
case 6: fill(242,179,43);stroke(242,179,43,95); break;
case 7: fill(238,90,49); stroke(238,90,49,100);break;
}
var x=xpos+i*50; //实际画的时候点的x坐标
var y=(t*78)%400+ypos+j*50; //实际画的时候点的y坐标
//如果点运动到画布下界,则从上界开始重新往下运动
if(y>=400)
y-=400;
//画点
ellipse(x,y,10,10);
//找直线与边界的交点坐标
var linex,liney; //交点的x、y坐标
//区域1
if ( x<200 && x>y )
{
linex=(200-x)/(y-200)*y+x;
liney=0;
}
//区域2
else if( x<200 && x<=400-y)
{
linex=0;
liney=(200-y)/(x-200)*x+y;
}
//区域3
else if ( x<200 && x>400-y )
{
linex=(200-x)/(y-200)*(y-400)+x;
liney=400;
}
//区域4
else if(x>200 && x<y)
{
linex=(200-x)/(y-200)*(y-400)+x;
liney=400;
}
//区域5
else if ( x>200 && x<=400-y)
{
linex=(x-200)/(200-y)*y+x;
liney=0;
}
//区域6
else
{
linex=400;
liney=(200-y)/(200-x)*(400-x)+y;
}
var times; //重复次数
switch(i) //每一列的“灯光”粗细不同
{
case 0: times=2;break;
case 1: times=4;break;
case 2: times=6;break;
case 3: times=25;break;
case 4: times=25;break;
case 5: times=6; break;
case 6: times=4; break;
case 7: times=2;break;
}
//画直线
//如果与竖直边界相交
if(linex==0 ||linex==400)
{
for (var m = 1; m <times ; m++)
{
line(linex,liney-m,x,y);
line(linex,liney+m,x,y);
}
}
//如果与水平底边相交
if(liney==400)
{
var times1=(2.0-times)*(x+y-400)/x+times;
for(var n=1; n< times1; n++)
{
line(linex-n/3,liney,x,y);
line(linex+n/3,liney,x,y);
}
}
//如果与水平顶边相交
if(liney==0)
{
var times2=(times-2.0)*y/x+2.0;
for(var k=1; k< times2; k++)
{
line(linex-k/3,liney,x,y);
line(linex+k/3,liney,x,y);
}
}
line(linex,liney,x,y);
}
}
4. 创意拓展
- 4.1 拓展一——时空穿梭,一切随机
对代码进行改造,我的第一个拓展主要是随机,比较简单,形成一个类似时空穿梭的画面。如图所示:
很多地方都用到了随机,主要用到的就是random()函数,它将产生一个在两个参数范围内的随机浮点数。
首先,为了模拟时空穿梭的炫光感,背景颜色需要在一定灰度范围内变化,这同时也增加了动态感:
//随机生成背景颜色
background(random(0,80),random(0,80),random(0,80));
为了模拟时空穿梭的黑洞,在边缘部分是灰色的椭圆组成的乱线,看上去像是黑洞。
//随机生成黑洞边缘
stroke(200,200,200);
var randx=random(300,800);
var randy=random(300,800);
noFill();
ellipse(200,200,randx,randy);
在画布中随机取光源点,半径也是在一个范围内随机,透明度同样也是随机,这里颜色没有随机,因为感觉原图中的颜色很好看,不想改动它。
//在画布中随机取点
var x=random(0,400);
var y=random(0,400);
//随机生成半径
var r=10+random(-10,30);
var opacity=random(0,255);
noStroke();
switch(i)
{
case 0: fill(238,33,90,opacity); break;
case 1: fill(198,33,132,opacity);break;
case 2: fill(99,41,140,opacity); break;
case 3: fill(16,115,188,opacity);break;
case 4: fill(0,165,148,opacity); break;
case 5: fill(132,198,66,opacity);break;
case 6: fill(242,179,43,opacity);break;
case 7: fill(238,90,49,opacity); break;
}
ellipse(x,y,10+r,10+r);
对于重复次数,不再是原来的固定值,而是由之前随机生成的圆的半径来决定,这样有一个好处,就是圆点越大的“光线”越强烈。
var times=r%40/2; //重复次数
后来发现好像没有圆点更像时空穿梭,下面补一个没有圆点的版本,只需把画圆点的代码去掉:
- 4.2 拓展二——炫彩光影,交互控制
对代码进行改造,采用键盘和鼠标的交互控制来实现灯光的实时变化,光源默认为方形,如果点击鼠标左键即可切换为圆形,按下任意键盘键即可放大光源且大小在一定范围内随机,看上去就有闪烁的效果,而且整个过程中,所有点光源都是根据鼠标的位置实时进行发射,而不是原来的画布中心点(200,200)了。效果图如下:
首先是切换点光源的类型,需要判断鼠标左键是否按下,如果按下,切换成圆形点,否则是方形点,下面代码很好理解。
if (mouseIsPressed)
{
//如果按下鼠标左键,切换灯光为方形
if (mouseButton === LEFT)
{
ellipse(x,y,r1,r2);
}
else
rect(x-r1/2,y-r2/2,r1,r2);
}
//画点
else
rect(x-r1/2,y-r2/2,r1,r2);
var times=(r1+r2)/5; //重复次数,光点越大,射线越粗
再用keyIsPressed判断是否按下了键盘,如果按下任意键盘键,就在一定范围内随机生成一个较大的点,因为每帧在刷新,这种随机就产生了一种“闪烁”的效果。
//按下任意键盘键,触发闪烁效果
if(keyIsPressed === true)
{
r1=random(30,50);
r2=random(30,50);
}
else
{
r1=10;
r2=10;
}
本拓展最为关键的在于实时跟随鼠标坐标进行光线放射,原图是以画布中心点为放射中心,改为鼠标实时点以后,区域的划分又改变了,对区域分界线的斜率计算稍微复杂了点,不再是简单的y=x和y=-x+400了,以下图为例,仍然分为6个区域:
代码的实现与原图类似,只是要把中心点坐标改为(mouseX,mouseY),每一个区域的斜率也需要改变,具体斜率计算与原图类似,这里不多阐述。
//找直线与边界的交点坐标
var linex,liney; //交点的x、y坐标
//区域1
if ( x<mouseX && x>mouseX/mouseY*y )
{
linex=(mouseX-x)/(y-mouseY)*y+x;
liney=0;
}
//区域2
else if( x<mouseX && x<=(400-y)*mouseX/(400-mouseY))
{
linex=0;
liney=(mouseY-y)/(x-mouseX)*x+y;
}
//区域3
else if ( x<mouseX && x>(400-y)*mouseX/(400-mouseY))
{
linex=(mouseX-x)/(y-mouseY)*(y-400)+x;
liney=400;
}
//区域4
else if(x>mouseX && x<(400-mouseX)/(400-mouseY)*(y-400)+400)
{
linex=(mouseX-x)/(y-mouseY)*(y-400)+x;
liney=400;
}
//区域5
else if ( x>mouseX && x<=(mouseX-400)/mouseY*y+400)
{
linex=(x-mouseX)/(mouseY-y)*y+x;
liney=0;
}
//区域6
else
{
linex=400;
liney=(mouseY-y)/(mouseX-x)*(400-x)+y;
}
5. 总结
本次实验初步尝试利用p5.js进行码绘,作品虽然有点儿青涩,但里面也凝结了我的思考与热爱生活的心~
通过本次临摹动态图形和自己的拓展作业,我更熟练地掌握了p5.js的应用,也加强了发现动态图形运动规律和利用数学方法和代码实现这些规律的能力,互动媒体的世界是奇妙的,通过码绘,我们可以进入一个我们从未设想过的世界,实现各种交互。只要用心,就能画出美美的图案!希望在后面的学习中能够有更多的发现与新的思考,加油!~