在科学中,对称性是指某种操作下的不变性或者守恒性,对称性常与守恒定律相联系。与空间平移不变性对应的是动量守恒定律;与时间平移不变性对应的是能量守恒定律;与转动变换不变性对应的是角动量守恒;与空间反射(镜像)操作不变性对应的是宇称守恒。在弱相互作用中,“宇称”不守恒,自然界在C或P下不是对称的,在CP下也不是对称的,但却是CPT对称的。这里C表示电荷变号操作,相当于反转变换,如由底片洗出照片,电子变正电子,物质变反物质;P表示镜像反射操作,如人照镜子;T表示时间反演操作,如微观可逆过程。也就是说,当同时把粒子与反粒子互变(C)、左与右互变(P)、过去与未来互变(T),自然界又是对称的。——百度百科
没错,我们生活中处处存在对称,对称也是我非常喜欢的一个性质,甚至对于可以那些明明可以对称但偏偏没有对称的东西有一种执念,就想它对称起来。所以我的创意编程灵感就来源于此。
图片想表现的正是对称的概念,正负相生,阴阳相容,用鼠标作过画的人都知道,别说画根直线或者画得像什么东西了,就写个字都奇丑无比,因为其难以控制。所以,这里鼠标体现的就是杂乱无章的运动,而通过对该运动的复制和对称演绎,实现了将无形无律之运动转变为有形有律之图案。看单个图案或许并不规则,但通过简单的旋转重复对称,整幅图案就变得有规律甚至让人觉得美妙动人。
①在图案中间有两个同心圆环,旋转方向相反;②周围有小球绕圆心做半径不断周期性变化的圆周运动,并有拖影效果;③当鼠标进入时,小球会离开当前位置并去追逐鼠标,该动作在50帧内完成;④鼠标在画布中运动时,小球跟随鼠标运动;⑤当鼠标离开画布时,小球离开当前位置前往自动旋转的位置,该动作也在50帧内完成;⑥通过上下方向键实现画布中小球数目的变化。
在P5中并没有找到直接画圆弧的函数,所以这里的实现很简单,就是利用先后绘画图案的遮盖关系,画一大一小两个扇形,一个亮色一个和背景色相同,即可实现圆弧的绘制。然后就是圆弧的简单堆砌。 圆弧旋转的实现用的是上一篇文章的方法,millis()函数记录了程序运行的时长,通过它来控制旋转,不再赘述。
var t = millis()/20; //draw the circle at the center for (var n=1; n<=numSlices; n++) { fill(0, 255, 255); for (var i=0; i<4; i++) { arc(0, 0, 120, 120, t+10+i*90, t+90-20+i*90); } fill(0); for (i=0; i<4; i++) { arc(0, 0, 110, 110, t+10+i*90-5, t+90-20+i*90+5); } fill(0, 255, 255); for (i=0; i<4; i++) { arc(0, 0, 80, 80, -t+10+i*90, -t+90-20+i*90); } fill(0); for (i=0; i<4; i++) { arc(0, 0, 70, 70, -t+10+i*90-5, -t+90-20+i*90+5); } }通过全局position数组记录小球的过往位置信息,并将其同步绘画在当前画布上,并按时间的先后顺序调整其相对大小(新点大旧点小)即可。 这段代码参考了,但优化了其代码,更正了逻辑错误也减少了没用的冗余。
function drawPoints(xs) { for (var i = xs.length - 1; i >= 0; i--) { var x = xs[i].x; var y = xs[i].y; var dia = diameter + diameterStep * (numPositions - i);//基础大小(即最小的)+变大步长 fill(0, 255, 255, 255 * (1 - i / numPositions)); ellipse(x - centerX, y - centerY, dia, dia); } }这里用了旋转画布的思想来实现其对称,在画布以规定的次数旋转一周后即可得到规定数目的小球,这里用numSlices变量来控制小球数量。 小球的圆周运动同样是借助millis()得到时间来实现,实现半径的变大变小是用了direction变量来控制其半径变化方向,到达规定边界后更改direction的值即可。
//小球自动运动时位置的寻找 if (direction_control == 0) { au_positions.unshift( { x: sin(-t*1.5) * range_control+400, y: cos(-t*1.5) * range_control+400, } ); range_control = range_control + 1; if (range_control>=300) { direction_control = 1; } } else { au_positions.unshift( { x: sin(-t*1.5) * range_control+400, y: cos(-t*1.5) * range_control+400, } ); range_control = range_control - 1; if (range_control<=100) { direction_control = 0; } } au_positions.splice(numPositions);//用来去掉过于陈旧的点,只保留规定数目的点然后调用画拖尾小球的函数,并将画布旋转规定次数即可。
for (var m = 1; m <= numSlices; m++) { rotate(360 / numSlices); drawPoints(au_positions); }和上文实现类似,只不过小球位置的记录不同。
positions.unshift( { x: mouseX, y: mouseY, } ); positions.splice(numPositions);同样旋转画布实现对称。
for (var i = 0; i < numSlices; i++) { drawPoints(positions); rotate(360 / numSlices); }首先判断鼠标是否在画布中(我的画布大小是800*800在左上角)。 然后判断鼠标是由画布外进入的还是一直都在画布中的。若由画布外进入则进行追逐动画。 实现: ①通过两个变量来控制,一个用来判断鼠标是否为由外部移入,一个用来判断追逐的开始(用来对小球追逐位置初始化),这里借鉴了操作系统中原子操作的思想,即做一个标记来标明我这步工作是否做完,我不不做完就不做下一步才能干的事情。 ②小球开始追逐的位置即为小球当前自动运动到的位置,然后将▲d设置为二者之间的差距/(50 - 已经运动帧数)的步长(小球会随鼠标运动越追越快),然后不断绘制小球每步的位置即可。需要注意的是:为了旋转方便我将绘图原点放在了画布中央,但是鼠标位置是以浏览器用户控制区左上角为原点的,所以这其中涉及到一个坐标转化问题(一个初中都应该会的坐标转化结果搞了一晚上……)。
if (mouseX<800 && mouseY<800) { if (mouse_first_2_out == 0) { mouse_first_2_out = 1; mouse_first_1_out = 1; } if (mouse_first_2_in == 1)//chase the mouse { if (mouse_first_1_in == 1) { x0 = int(400-au_positions[0].x); y0 = int(400-au_positions[0].y); mouse_first_1_in = 0; } if (chase_num_in < 50) { ddx = (0-(mouseX-400)-x0) / (50 - chase_num_in); ddy = (0-(mouseY-400)-y0) / (50 - chase_num_in); x0 = x0 + int(ddx); y0 = y0 + int(ddy); var dia = diameter + diameterStep * numPositions; fill(0, 255, 255); for (var m = 1; m <= numSlices; m++) { rotate(360 / numSlices); ellipse(-x0, -y0, dia, dia); } chase_num_in++; } else { mouse_first_2_in = 0; chase_num_in = 0; //print('have chase the mouse'); } } else { // For each slice for (var i = 0; i < numSlices; i++) { drawPoints(positions); rotate(360 / numSlices); } } }这里和鼠标进入时极为相似,将其反向即可。
else { if (mouse_first_2_in == 0) { mouse_first_2_in = 1; mouse_first_1_in = 1; } if (mouse_first_2_out == 1)//chase the mouse { if (mouse_first_1_out == 1) { x0 = -int(mouseX-400); y0 = -int(mouseY-400); mouse_first_1_out = 0; } if (chase_num_out < 50) { ddx = (0-(au_positions[1].x-400)-x0) / (50 - chase_num_out); ddy = (0-(au_positions[1].y-400)-y0) / (50 - chase_num_out); x0 = x0 + int(ddx); y0 = y0 + int(ddy); var dia = diameter + diameterStep * numPositions; fill(0, 255, 255); for (var m = 1; m <= numSlices; m++) { rotate(360 / numSlices); ellipse(-x0, -y0, dia, dia); } chase_num_out++; }else { mouse_first_2_out = 0; chase_num_out = 0; //print('have chase the mouse'); } } else { for (var m = 1; m <= numSlices; m++) { rotate(360 / numSlices); drawPoints(au_positions); } } }这里用键盘响应函数实现小球数目的增减。需要注意的是keyIsDown()函数会对你的按键操作响应一次,完成规定操作,而keyIsPressed()函数则只会在按下相应按键的时段内进行响应,在放开按键后会恢复成按键前的样子。
if(keyIsDown(UP_ARROW)) { numSlices++; time_control_rull = time_control; } else if(keyIsDown(DOWN_ARROW)) { numSlices--; time_control_rull = time_control; }这里还有个问题,即更新速度太快了,往往按以下后会一下子响应好多次。所以加入了按键时间间隔判断,如果这次按下的时间在上次按下的0.5秒之后了才行。优化代码如下:
time_control = millis(); //前面还有一个定义了初始值的time_control_rull。 //print(time_control - time_control_rull); if(time_control - time_control_rull >=500) { if(keyIsDown(UP_ARROW)) { numSlices++; time_control_rull = time_control; } else if(keyIsDown(DOWN_ARROW)) { numSlices--; time_control_rull = time_control; } }这是我第一个P5小三百行的程序,能实现自己心里所想的功能还是很有成就感的,但是创作时长达十个小时,这是让我没有想到的。其实总结写起来很简单,但做的时候真是大费周章。我认为有两点值得反思学习,首先,头脑中蓝图要清晰,我的蓝图真正清晰起来是在编写了三个小时左右的时候。其次,代码实际上就是逻辑和数学的体现,公式一定要推导清楚,不要急于敲代码尝试!因为很浪费时间。我发现当我敲代码时总是想先敲出来试试,导致思考总是仓促的、马虎的、想当然的、不周到的,导致一直在出错,这无疑是浪费时间的,是应该避免的。