查看原文
其他

写给设计师的OF编程指南(4)- 图形的运动(下)

2016-05-26 Wenzy InsLab

运动与函数

在大多人的印象里,数学好像没什么用,在日常生活里用得最多的也仅仅是加减乘除。

但如果你是在用程序做创作,情况就大不一样。了解越多,越能玩出花样。

先放几张的不明觉厉的图挑逗大家的兴致。



这是什么?现在先不剧透,后面你会亲自用上它。

上一节,我们了解了 setup 函数和 draw 函数,这使得静止的图形可以运动起来。但这种运动形式太朴素了,我们要用上以前掌握的函数知识,让图形跑出自己的个性。



上面的数学函数还能认出多少?它们与运动有何关系?

先从中选一个二次函数,同时添加一些参数看看,比如 y = x² / 100


它的函数图像是这样的,复制下面这段代码

--代码示例(3-1):

—- ofApp.h内  —-

   float x, y;

—- ofApp.cpp内  —-

   void ofApp::setup(){        ofSetWindowShape(300, 300);        ofSetBackgroundAuto(false);        ofBackground(0);        x = 0; // 此句可省略,变量声明后,初值默认为 0    }    void ofApp::update(){        x++;    }    void ofApp::draw(){        ofSetColor(255); // 此句可省略,填充色彩默认为白色        y = pow(x,2) / 100.0;        ofDrawCircle(x,y,1);    }


运行效果。

接着再选一个 sin 函数, y = 150 + sin(x)


复制下面这段代码。

--代码示例(3-2):

—- ofApp.h内  —-

   float x, y;

—- ofApp.cpp内  —-

   void ofApp::setup(){        ofSetWindowShape(300, 300);        ofSetBackgroundAuto(false);        ofBackground(0);    }    void ofApp::update(){        x++;    }    void ofApp::draw(){        y = ofGetHeight()/2 + sin(ofDegToRad(x)) * 150;  //ofDegToRad 函数将角度值转成弧度值        ofDrawCircle(x,y,1);    }

运行效果:


以上是它们的运动轨迹。对照着上图和下图,结果是显而易见的。函数图像其实就对应着运动轨迹。相当简单,只要将函数中的 x,y 的值替换进绘图函数的横纵坐标中就能绘制出图像。第一张绘制的轨迹,其实就等价于函数 y = x² / 100 的图形。而第二张的轨迹,等价于 y = 150 + sin(x) 。只是在程序中,由于 y 轴方向是反的,所以与原图相比,图形轨迹会上下颠倒。

现在应该有种阔然开朗的感觉。以前学习的各式稀奇古怪的函数,原来可以在程序中控制图形的运动!

数学函数在程序中怎么写?

下面罗列了一些使用频率很高的函数,可以帮助我们将数学函数翻译成计算机能识别的代码

函数名作用格式
abs返回指定数的绝对值abs(x)
log返回以自然对数 e 为底,n 的对数log(n)
sqrt返回指定数的平方根sqrt(x)
pow返回指定数的 n 次方pow(x,n)
exp返回自然对数 e 的 n 次方exp(n)

因此下面这些式子,在程序中就可以这样写。

y = x²  →   y = pow(x, 2)

y = x³  →   y = pow(x, 3)

y = xⁿ  →   y = pow(x, n)

y = 4ⁿ  →   y = pow(4, n)

y =logₑ² →  y = log(2)

y = e² → y = exp(2)

y = √5 → y = sqrt(5)

你可以尝试在程序里写个函数,观察它的运动轨迹。但请记得考虑函数的值域和定义域的范围,否则你画的图很可能会跑在屏幕之外。

三角函数

下面再来了解一下与三角函数相关的函数写法

函数名作用格式
sin正弦函数sin(x)
cos余弦函数cos(x)
tan正切函数tan(x)
ofDegToRad将角度值转化成弧度值ofDegToRad(x)
ofRadToDeg将弧度值转化为角度值ofRadToDeg(x)


值得注意的是,在程序中,与角度相关的函数参数输入采取的是弧度制。所以 sin90° ,应该写成 sin(PI/2)。如果不熟悉这种方式,也可以用 ofDegToRad 函数将角度先转换为弧度 ,写成 sin(ofDegToRad(90))。

ofRadToDeg 函数的作用恰恰相反,可以将弧度值转化为角度值。尝试在 setup 中写下这行代码。看看结果会是多少?

   cout << ofRadToDeg(PI/2) << endl;

用三角函数控制图形运动

下面给出一个范例,看看实际的图形运动效果

--代码示例(3-3):

—- ofApp.h内  —-

   float x, y;

—- ofApp.cpp内  —-

   void ofApp::setup(){        ofSetWindowShape(700, 300);        ofBackground(234, 113, 107);    }    void ofApp::update(){        x++;    }    void ofApp::draw(){        y = sin(ofDegToRad(x)) * 150 + 150;        ofDrawCircle(x,y,25);    }


  • sin函数是周期函数,最小值是-1,最大值为 1 。屏幕高度为 300 。根据 y = sin(ofDegToRad(x)) * 150 + 150; ,因此 y 值的变化范围就会刚好控制在 0 到 300 之内。

旋转的圆

这节的重头戏到了。那我们怎样在程序中画一个圆的轨迹?可以用什么函数去表示?再次搬出这两张图~~


它们其实很直观地揭示了圆周坐标与三角函数的关系。图上所有的运动,都是通过不断增大自变量 θ 来驱动的。左边其实就是 sin 函数与 cos 函数的图像,右边代表的是经过映射后,一个作圆周运动的点。现在看起来一点也不神秘了,你还可以用代码去实现它!

--代码示例(3-4):

—- ofApp.h内  —-

   float x, y, r, R, angle;

—- ofApp.cpp内  —-

   void ofApp::setup(){        ofSetWindowShape(300, 300);        ofBackground(234, 113, 107);        r = 10;          //圆的半径        R = 100;        //运动轨迹的半径    }    void ofApp::update(){        angle += 0.05;        x = ofGetWidth()/2 + R * cos(angle);        y = ofGetHeight()/2 + R * sin(angle);    }    void ofApp::draw(){        ofDrawCircle(x,y,r);    }


一个旋转的圆出现了。在这里,自变量不再是不断递增的 x ,而是变成了 angle( 也相当于例图中的 θ )。它代表角度。其中 xy 都分别乘以系数 R,也就相当于扩大了圆的运动半径( R 代表运动半径 )。若是不乘以 R ,它的图形变化轨迹只会局限在 -1 到 1 的范围。前面加上 ofGetWidth()/2 和 ofGetHeight()/2 相当于将旋转中心平移到画布中央。


但有没有想过,为什么不能像前面的函数一样,只用一个用不断递增的 x 来画出图形?根据函数自身的特性,定义域中任意 x ,有且只有一个 y 与之相对应。所以在平面直角坐标系里,你无法寻找到一个“简单函数”直接画出圆。

不能用这种形式

y = (包含x的神秘表达式?) ; x++ ;

所以才需要拐个弯,找一个 angle 来作为自变量。并用 sin 和 cos 函数将它转化为横纵坐标。

x = R * cos(angle); y = R * sin(angle); angle += 0.05;

当然有人会好奇,为何这样就能表示圆的运动轨迹?根据三角函数的定义其实是不难推出的。sin 函数是对边与斜边之比,cos 函数是邻边与斜边之比。无论圆周上的点位置在哪,r(半径)都是不变的。因此可以得到 x 坐标与 y 坐标的表达式


由于这个不是数学指南,有关三角函数的知识,就不在这里展开了。如果确实忘记,后面也会有相关的章节去回顾。

当然,不完全理解也没有任何问题,只要知道怎么用它画圆即可。这也是一种“编程思维”,以后我们常常需要调用一些别人做好的模块来实现某种功能。所以无需有强迫症的心态,非去搞清里面的细节。

但 sin 和 cos 在创意编程中太常用了,若是想进行更高阶的创作,尽量把它想明白。



上面几张动图,都与三角函数密切相关。

运动的坐标系

之前的效果,都是图形坐标在变化,坐标系本身是静止的。其实我们可以通过让坐标系动起来,来实现运动效果。这就好比岸上的人看船上的人,船上的人相对船是静止,但若是船本身在动,从岸上看去,人也就动了。所以前面讲的例子,一直都是“人在船上跑”,船并没有动。

下面是变换坐标系的常用函数

函数作用
ofTranlate(x, y)平移坐标系
ofScale(a)缩放坐标系
ofRotate(a)旋转坐标系
ofPushMatrix(), ofPopMatrix()变换堆栈(存取坐标系)

ofTranslate函数

ofTranslate函数前面有提到过,用于平移图形的坐标系

调用形式:

   ofTranslate(a, b)

第一个参数代表往 x 轴的正方向移动 a 个像素,第二个参数代表往 y 轴的正方形移动 b 个像素。

  • 以下实例的窗口大小设置为(100 X 100) , 对比两段代码,观察有何不同

--代码示例(3-5):

使用前:

   void ofApp::draw(){        ofDrawCircle(0,0,10);    }


使用后:

   void ofApp::draw(){        ofTranslate(50,50);        ofDrawCircle(0,0,10);    }


ofRotate函数

调用形式:

   ofRotate(a)

函数用于旋转坐标系 ,当参数为正数,会以原点为中心,往顺时针方向旋转。传入的参数和三角函数一样,采取弧度制。

--代码示例(3-6):

使用前:

   void ofApp::draw(){        ofDrawCircle(50,50,10);    }


使用后:

   void ofApp::draw(){        ofRotate(30);        ofDrawCircle(50,50,10);    }


在程序中产生的作用,就是让圆围绕坐标原点,顺时针旋转 30 度。


ofScale函数

调用形式:

   ofScale(a,b)

函数可以缩放坐标系,参数 a 缩放 x 轴方向,参数 b 缩放 y 轴方向。
数值大小代表缩放的倍数。大于 1 放大,小于 1 则缩小。

--代码示例(3-7):

使用前:

   void ofApp::draw(){        ofDrawCircle(0,0,10);    }


使用后:

   void ofApp::draw(){        ofScale(4,4);        ofDrawCircle(0,0,10);    }


上图的圆就放大到原来的四倍了。你也可以使用两个参数,分别

   void ofApp::draw(){        ofScale(4,2);        ofDrawCircle(0,0,10);    }


变换函数在 OF 与 P5 中的异同对比

  • P5 中的 translate,rotate,scale 分别对应 OF 中的 ofTranslate, ofRotate, ofScale

  • P5 中变换函数的参数输入采取弧度制,OF 中则采取角度制。

  • P5 中的 scale 允许输入单个参数,OF 中的 ofScale 参数数量必须为 2 个或 3个。

变换函数的叠加

这里的变换都是相对当前坐标系的变换。换句话说,效果是可以叠加的。

--代码示例(3-8):

   void ofApp::draw(){        ofTranslate(40, 10);        ofTranslate(10, 40);        ofDrawCircle(0,0,10);    }

最终效果就等价于

   void ofApp::draw(){        ofTranslate(50, 50);        ofDrawCircle(0,0,10);    }


ofRotate函数也一样

--代码示例(3-9):

   void ofApp::draw(){        ofRotate(10);        ofRotate(20);        ofDrawCircle(50,50,10);    }

等价于

   void ofApp::draw(){        ofRotate(30);        ofDrawCircle(50,50,10);    }


由于 ofScale 和 ofRotate,都是以原点为中心进行缩放和旋转的。当我们希望一个中心位置在(50,50)的图形产生旋转的效果。就需要倒过来思考,首先将坐标原点移动到(50,50)的位置,再添加旋转变换函数,最后才把图形绘制在原点上。

--代码示例(3-10):

使用前:

   void ofApp::draw(){        ofDrawEllipse(50, 50, 50, 20);    }


使用后:

   void ofApp::draw(){        ofTranslate(50,50);        ofRotate(45);        ofDrawEllipse(0, 0, 50, 20); //为了看出旋转的角度变化,绘制一个椭圆    }


平移与圆周运动

下面的例子会通过变换坐标系来实现运动效果。很多时候,在程序中实现特定的效果,完全可以用截然不同的手法。

平移运动

--代码示例(3-11):

—- ofApp.h内  —-

   int x,y;

—- ofApp.cpp内  —-

   void ofApp::setup(){        ofSetWindowShape(300,300);        y = ofGetHeight()/2;    }    void ofApp::update(){        x++;    }    void ofApp::draw(){        ofBackground(234, 113, 107);        ofTranslate(x,y);        ofDrawCircle(0,0,25,25);    }


圆的绘制坐标本身没有变化,被改变的是所在的坐标系。

旋转运动

--代码示例(3-12):

—- ofApp.h内  —-

   float r, R, angle;

—- ofApp.cpp内  —-

   void ofApp::setup(){        ofSetWindowShape(300,300);        r = 10;        R = 100;    }    void ofApp::update(){        angle ++;    }    void ofApp::draw(){        ofBackground(234, 113, 107);        ofTranslate(ofGetWidth()/2,ofGetHeight()/2);        ofRotate(angle);        ofDrawCircle(0, R ,r);    }


是否比用三角函数画圆更简洁,也更易理解了?这里有人可能会有疑问,以旋转运动的代码为例。前面提过的变换函数明明是相对的,而且允许叠加效果。那 ofTranslate(ofGetWidth()/2,ofGetHeight()/2);  写在 draw 函数里,岂不代表 draw 函数每运行一次,坐标系都会在原基础上往右下方移动一段距离,理论上是不会永远保持在屏幕中心?

可以这样去理解,draw 函数里的代码只要由上到下跑完一次,第二次循环时坐标系都会回到始初状态,坐标系的原点会默认回到左上角上。所以要想坐标系维持持续的变化,ofRotate 函数中的 angle 数值,就需要不断递增。

存取坐标状态

有些时候,我们不希望坐标系的状态是在之前的基础上变换。这时就要用到 ofPushMatrix 和 ofPopMatrix 。这两个函数是成对出现的,ofPushMatrix 在前 ofPopMatrix 在后。不能单独使用,否则就会出错。

--代码示例(3-13):

   void ofApp::draw(){       ofPushMatrix();    //保存坐标系状态        ofTranslate(50, 50);        ofDrawCircle(0, 0, 10);        ofPopMatrix();     //读取坐标系状态        ofDrawRectangle(0, 0, 20, 20);    }


例子中,在使用 ofTranslate(50,50) 前,先用 ofPushMatrix。就会保存坐标系当前的状态,这同时也是坐标原点在左上角的初始状态。当绘制完圆形后,再执行 popMatrix ,就会还原到到这个状态。此时再执行 ofDrawRectangle ,会发现它没有受到 ofTranslate 的影响。而是在左上角的原点上绘制了一个正方形。

另外,ofPushMatrix 和 ofPopMatrix 是允许嵌套使用的。

例如

pushMatrix(); ...    pushMatrix();    ...    popMatrix();    ... popMatrix(); ...
  • 为了更直观地表明对应关系,采取了缩进的形式

组合运动,运动中的运动?

前面用船和人的例子作比喻。是否有想过,如果船上的人和船都动起来,岸上的人看过去会是怎样的一番体验?

假如平移运动与坐标系的旋转运动组合到一起?这里的点其实只朝一个方向运动~

--代码示例(3-14):

—- ofApp.h内  —-

   int x, y;    float angle;

—- ofApp.cpp内  —-

   void ofApp::setup(){        ofSetWindowShape(300,300);        ofBackground(234, 113, 107);        ofSetBackgroundAuto(false);    }    void ofApp::update(){        angle += 15;        y--;    }    void ofApp::draw(){        ofTranslate(ofGetWidth()/2, ofGetHeight()/2);        ofPushMatrix();        ofRotate(angle);        ofDrawCircle(x, y, 3);        ofPopMatrix();    }


也可以是圆周运动与坐标系的缩放运动的组合~

--代码示例(3-15):

—- ofApp.h内  —-

   float x, y, angle;

—- ofApp.cpp内  —-

   void ofApp::setup(){        ofSetWindowShape(300,300);        ofBackground(234, 113, 107);        ofSetBackgroundAuto(false);    }    void ofApp::update(){        angle += 0.01;        x = sin(angle) * 100;        y = cos(angle) * 100;    }    void ofApp::draw(){        ofTranslate(ofGetWidth()/2, ofGetHeight()/2);        ofPushMatrix();        ofScale(1 + 0.1 * sin(angle * 10),1 + 0.1 * sin(angle * 10));        ofDrawCircle(x, y, 3);        ofPopMatrix();    }


可被最终的结果欺骗了,在程序中圆点只在做圆周运动。这个坐标系的缩放可以用摄像头去类比,一个不断前后运动的摄像头在拍摄一个作圆周运动的点。

以上都是非常简单的基础函数,但通过不同组合,效果却可以千差万别。之后就不透露太多了,怎能剥夺大家探索的乐趣?

综合运用

这两节指南较详细地介绍了图形运动的基本方法。相信你对运动的理解,比以前更深了。最后给出一些完整的实例供大家参考。

--代码示例(3-16):

—- ofApp.h内  —-

   float x1, y1, x2, y2, r, R;    float angle1, angle2;

—- ofApp.cpp内  —-

   void ofApp::setup(){        ofSetWindowShape(300,300);        ofSetBackgroundAuto(false);        r = 6;        R = 120;    }    void ofApp::update(){        angle1 += 0.02;        angle2 += 0.06;        x1 = R * sin(angle1);        y1 = R * cos(angle1);        x2 = R/2 * sin(angle2);        y2 = R/2 * cos(angle2);    }    void ofApp::draw(){        ofBackground(234, 113, 107);        ofTranslate(ofGetWidth()/2, ofGetHeight()/2);        ofDrawCircle(x1, y1, r/2);        ofDrawCircle(x2, y2, r);        ofDrawCircle(-x1, -y1, r/2);        ofDrawCircle(-x2, -y2, r);        ofDrawCircle(x1, -y1, r/2);        ofDrawCircle(x2, -y2, r);        ofDrawCircle(-x1, y1, r/2);        ofDrawCircle(-x2, y2, r);        ofSetLineWidth(2); // 设置线条粗细        ofDrawLine(x1, y1, x2, y2);        ofDrawLine(-x1, -y1, -x2, -y2);        ofDrawLine(x1, -y1, x2, -y2);        ofDrawLine(-x1, y1, -x2, y2);    }


这个例子涉及的函数知识都没有超出前面的。

是不是搞不清哪个点对哪个点?哪条线对哪条线?其实我自己也搞不清楚……但我还记得它是由一小段代码衍生而来的。


这就是它的运动本质。其余的线条仅仅是镜像效果而已。

如果你继续跟随这个指南,后期还可以做一个升级版,给图形添加控件,来实时地改变图形的运动状态。


编程的有趣之处就在于可以设计规则,组合规则。但最终能写成什么程序,就看自己的造化了。设计师往往有很强的图形想象力,你既可以先在脑中勾勒出动态草图,再设法从脑中“翻译”成代码。也能从代码和法则本身出发,随意设计函数和变量。在编程世界,代码就是你的画笔!用它挥洒自己的创意吧~~

END

最后穿越回去解答一个之前的遗留问题吧。我们那么费力地用程序画一张图,究竟有有何作用?学完这章以后,有太多玩法了。

--代码示例(3-17):

—- ofApp.h内  —-

   float browX, earR, eyeR;

—- ofApp.cpp内  —-

   void ofApp::setup(){        ofSetWindowShape(500,500);        ofSetBackgroundAuto(false);    }    void ofApp::update(){        eyeR = 30 + sin(ofGetFrameNum() / 30.0) * 25;        earR = 90 + sin(ofGetFrameNum() / 10.0) * 10;        browX = 150 + sin(ofGetFrameNum() / 30.0) * 20;    }    void ofApp::draw(){        //背景色设置与线条宽度设置        ofBackground(200, 0, 0);        ofSetLineWidth(8);        // 耳        ofFill();        ofSetColor(255);        ofDrawCircle(175, 220, earR);        ofDrawCircle(ofGetWidth() - 175, 220, earR);        ofNoFill();        ofSetColor(0);        ofDrawCircle(175, 220, earR);        ofDrawCircle(ofGetWidth() - 175, 220, earR);        // 脸        ofFill();        ofSetColor(255);        ofDrawRectangle(100, 100, 300, 300);        ofNoFill();        ofSetColor(0);        ofDrawRectangle(100, 100, 300, 300);        // 眉毛        ofDrawLine(browX, 160, 220, 240);        ofDrawLine(ofGetWidth() - browX, 160, ofGetWidth() - 220, 240);        // 左眼        ofFill();        ofSetColor(ofRandom(255),ofRandom(255),ofRandom(255));        ofDrawCircle(175, 220, eyeR);        ofNoFill();        ofSetColor(0);        ofDrawCircle(175, 220, eyeR);        // 右眼        ofFill();        ofSetColor(ofRandom(255),ofRandom(255),ofRandom(255));        ofDrawCircle(ofGetWidth() - 175, 220, eyeR);        ofNoFill();        ofSetColor(0);        ofDrawCircle(ofGetWidth() - 175, 220, eyeR);        // 嘴        ofFill();        ofSetColor(255);        ofDrawTriangle(170, 300, ofGetWidth() - 170, 300, 250, 350);        ofNoFill();        ofSetColor(0);        ofDrawTriangle(170 - cos(ofGetFrameNum() / 10.0) * 20, 300 - sin(ofGetFrameNum() / 10.0) * 20, ofGetWidth() - (170 + cos(ofGetFrameNum() / 10.0) * 20), 300 + sin(ofGetFrameNum() / 10.0) * 20, 250, 350);        // 鼻        ofFill();        ofSetColor(0);        ofDrawCircle(ofGetWidth()/2, ofGetHeight()/2,4);    }


动图是不是比较魔性?这里不做太多文章了。留待你去设计更棒的效果。

用程序去画图,它的优势在于,可以真正做到把玩每个像素。由于绘制的不是位图,所以图上的每个关键点都是可控的,能由此实现一些其他软件难以达到的效果。

如果你有一颗想肢解一切,又重组一切的心,学习编程一定可以最大程度地满足你。



Processing版:

写给设计师的趣味编程指南-(4)让图形跑起来(下)


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存