一直反反复复以为自己已经了解傅里叶变换了,互动媒体课上重新因为它在诸多场景中的应用有了新的认识。
悸动是一个可以点击生成小圆,根据点击的小圆的时间与次数决定这个波的周期与振幅,当点击停止后小圆会加入到振荡的大圆中,而大圆的半径由组成它的小圆的实时值相加得到。
写的时候以为自己在一本正经写傅里叶变换,写完以后才意识到并不是,只是一个波的叠加,是傅里叶变换可以从复杂的波中分出简单的正弦波的理论基础。
本项目由三个类组成:点击生成的小圆类,不停振荡的大圆类与圆的控制类。
由于部分代码与振荡的主题并不相关,而且不是非常有意思,所以只贴上源码,不作过多解释。
点击生成小圆
在鼠标点击处生成小圆,每次点击都会使小圆增大,同时第一次点击生成小圆的时间到最后一次点击使小圆增大的时间作为这个小圆代表的正弦波的周期的四分之一。
距离最后一次点击过去50帧后还没有点击,则视为小圆停止增长。
class circle {
PVector center, target;
int r, t, initialPhase;
int startFrame, lastFrame;
int isGrow;
int isJoin;
color c;
circle(PVector mouse) {
center = mouse;
r = 0;
isGrow = 10;
isJoin = 0;
startFrame = frameCount;
lastFrame = frameCount;
t= 10;
c = color(floor(random(255)), floor(random(255)), floor(random(255)));
}
void grow() {
isGrow += max(10, r / 20);
t = (frameCount - startFrame) * 4;
lastFrame = frameCount;
}
void growPerFrame() {
r++;
isGrow--;
}
void update() {
if(isGrow > 0) {
growPerFrame();
}
if(isJoin == 1) {
center.add(PVector.sub(cc.center, center).div(6));
if(PVector.sub(cc.center, center).mag() < 5) {
isJoin = -1;
}
}
if(isJoin == 0 && isGrow ==0 && (frameCount - lastFrame) > 50) {
isJoin = 1;
}
fill(c);
ellipse(center.x, center.y, r, r);
fill(255);
textSize(22);
text("new circle:", width - 120, 120);
text("r : " + r, width - 120, 150);
text("t : " + t, width - 120, 180);
}
}
控制类
每次点击时若在目前已有的正在增长状态的某个小圆里,则使这个小圆再次增长;否则生成一个可以增长的小圆。
每帧对所有正在增长的小圆currentCircles更新,使小圆增长,并将一段时间里没有被点击的小圆从增长的小圆的currentCircles中移除并将其加入到大圆centerCircle中去。
class circleManagement {
ArrayList<circle> currentCircles;
circleManagement() {
cc = new centerCircle();
currentCircles = new ArrayList<circle>();
}
void clicked(PVector mouse) {
boolean isMatched = false;
for(circle c : currentCircles) {
if(c.isJoin == 0 && PVector.dist(c.center, mouse) < c.r) {
c.grow();
isMatched = true;
break;
}
}
if(!isMatched) {
currentCircles.add(new circle(mouse));
}
}
void update(){
for(int i = currentCircles.size() - 1; i >= 0; i--) {
currentCircles.get(i).update();
if(currentCircles.get(i).isJoin == -1) {
cc.join(currentCircles.get(i));
currentCircles.remove(currentCircles.get(i));
}
}
}
}
振荡的大圆
每一帧中大圆的半径由所有汇入大圆的小圆在此时的半径叠加计算得出。
r = 0;
for(circle c : circles) {
r += (1 + sin((float)(frameCount - c.initialPhase) % c.t / c.t * 2 * PI)) * c.r;
}
其中小圆半径与周期的确定规则在第一部分已经写明。而当小圆汇入大圆时,周期在PI/2。
class centerCircle {
PVector center;
int r, t;
color myColor;
ArrayList<circle> circles;
int[] history;
int point, m;
centerCircle() {
center = new PVector(width / 2, height / 2);
r = 0;
t = 0;
myColor = color(0);
circles = new ArrayList<circle> ();
m = max(width / 2, 360);
history = new int[m];
for(int i = 0; i < m; i++) {
history[i] = 0;
}
point = 0;
}
void update() {
r = 0;
for(circle c : circles) {
r += (1 + sin((float)(frameCount - c.initialPhase) % c.t / c.t * 2 * PI)) * c.r;
}
point = (point + 1) % m;
history[point] = r;
fill(myColor);
ellipse(center.x, center.y, r, r);
drawPolar();
drawLine();
fill(255);
textSize(22);
text("circle:", width - 120, 30);
text("r : " + r, width - 120, 60);
}
void join(circle c) {
c.initialPhase = frameCount;
circles.add(c);
myColor = myColor + c.c;
t += c.t;
}
}
绘制波形
为了能够实时看到大圆半径,即所有小圆在此时半径叠加的结果的变化过程,我们将一段时间内大圆半径的变化绘制出来。
在这里,实现的方法是,在一个大小为半个屏幕width/2的整型数组中存储近width/2帧大圆的大小,每一帧将这些大小在第0到width/2列像素上绘制出来,就可以得到波形。
由于缺乏经验,不知道通常是怎么实现的,为了节约内存,在这里我自己使用了一个类似队列的方法:使用恒定的大小为width/2的整型数组,同时使用一个整型数值作为指针。
int[] history;
int point, m;
m = width / 2;
history = new int[m];
for(int i = 0; i < m; i++) {
history[i] = 0;
}
point = 0;
每一帧把数值更新在指针处,并更新指针位置。指针所指的位置是距此时时间最久的半径数值。因此可以最大地利用空间。
for(circle c : circles) {
r += (1 + sin((float)(frameCount - c.initialPhase) % c.t / c.t * 2 * PI)) * c.r;
}
point = (point + 1) % m;
history[point] = r;
void drawLine() {
stroke(255);
beginShape(LINES);
for(int i = 0; i < width / 2; i++){
vertex(width / 2 - i, height / 2 - history[(point - i + m) % m] / 2);
}
endShape();
noStroke();
}
}
结果分析
由于一开始就想要将波形的变化可视化出来,而且将这个过程变得有趣,所以在写完代码以后玩了很久,发现了一些规律。
-
波形的周期越长,变化越迟缓;周期越短,变化越剧烈。
-
不同的波叠加时,只要振幅与周期完全相同,相位相差180就可以抵消
-
周期与相位完全相同,原本的波形会得到增强,但不会有新的波峰产生
-
小振幅短周期的波会引起波形的抖动,而大振幅长周期的波则主要对波的整体形状产生影响
-
多种波叠加时,波形会变得非常随机,并且可能出现剧烈的变化
一些bug
本来还想绘制极坐标中的波形变化图,在这个探索过程中产生了一些bug,但这些bug视觉效果还非常不错(也就盯着看了大半个小时),所以一并在博文里记录下来。
阅读错误代码,可以看到是因为刚开始忘了processing中的三角函数的参数是弧度制的,将(0,360)的数传了进去,所以绘制了许多层。
由于产生的半径数据比较随机,所以动态效果并不规律,随机性比较强,所以异常好看。
void drawPolar() {
stroke(255);
beginShape(LINES);
for(int i = 0; i < 360; i++){
println(history[(i + point) % m] * cos(i));
vertex(history[(i + point) % m] * sin(i) + width / 2, history[(i + point) % m] * cos(i) + height / 2);
}
endShape();
}
正确代码如下:
(但极坐标做出来的视觉效果不知道为什么不是特别好,所以最后没有采用)
(可能是时间方面的问题)
vertex(history[(i + point) % m] * sin((float)i / 360 * TWO_PI) + width / 2, history[(i + point) % m] * cos((float)i / 360 * TWO_PI) + height / 2);
写到这里突然觉得用波形叠加的方法生成的数值连续又随机,可以根据大周期大振幅与小周期小振幅来调整它的形状,非常适合生成连续的随机数,甚至可以用来做地形,甚至想着可以自己写一个随机数生成器以后用。
然后灵光一闪,想到之前游戏课老师提到过柏林噪声的原理,好像也不复杂,去查了查,发现就是用的这个方法QAQ