查看原文
其他

用代码画画-详解三角函数

2017-05-02 Wenzy InsLab

用代码画画,必需要懂很多数学知识?如果数学基础没那么好,是否就无法肆意表达,领略其中的乐趣?

其实不然。很多时候,只要用简单的数学知识,也能做出复杂精妙的作品。

希望通过下文,可以让你破除对数学的恐惧,从基础的概念入手,与三角函数“共舞”。

什么是三角函数?

如果要问哪个数学函数在图形创作上使用频率最高,那三角函数估计能排在前几位。

三角函数,用简单的话来讲,是用来描述直角三角形边长和角度关系的函数。常用的三角函数有正弦函数( sin ),余弦函数( cos ),正切函数( tan )。

用一张图来举例。假如有一个直角三角形 ABC,其中 a,b 是直角边,c 是斜边。

那么 ∠B (角B)的正弦函数可以写作 sin(B),它的值就是 ∠B 的对边除以斜边,即 b 除以 c。∠B 的余弦函数写作 cos(B),它的值则是 ∠B 的邻边除以斜边,即 a 除以 c。而正切函数,可以写作 tan(B),它是 ∠B 的对边除以邻边,即 b 除以 a

上面所谓的斜边,指的就是直角三角形最长的那条边。而对比和邻边的概念是相对的,对边是指某个角对面的那条直角边,邻边就是某个角相邻的直角边。

我们在中学时期学习过三角函数,可能对“ 对比邻 ”,“ 邻比斜 ”,“ 对比斜 ”这几个词有印象,他们就是分别用来记忆 sin,cos,tan 的口诀。之后的程序中我们主要会重点介绍 sin 和 cos ,所以只要记住 sin 是“ 对比斜 ”,cos 是“ 邻比斜 ”即可。

关于三角函数我们可以记住这样一个性质:直角三角形中,边与角的这种比例关系是固定的,所以无论是多大或多小的三角形,只要两个三角形比例相似,相对应两个角的 sin 值,总是恒定的,因为边长的比例固定。反过来,如果我们已知某三角形某个角的 sin,cos 或 tan 值,结合一定条件,也能反过来推算出角度值。

为了更好地理解,这里再拿一个特殊的直角三角形来举例。假如一个直角三角形它的各个角分别为 30 度,60 度,90 度。那它的对边之比,就分别为 1:√3:2。

现在,我们只要将各边对应的比例关系代入,就能手动计算出正弦函数和余弦函数的数值。比如 sin 30 度就为 1/2 ,即 0.5。cos 30 度就为 √3/2,约等于 0.866。

这就是三角函数的数学定义。看到这里,相信你已经对它有了基本的了解,接下来我们可以学习在程序上的使用方法。

在 Processing 中使用三角函数

假如我们现在需要用程序来直接获取 sin 30 度的值,可以在 Processing 中这么写。

println(sin(PI/6));

输出结果为 0.5

cos 30 度,则是这么写

println(cos(PI/6));

结果约等于 0.866

但为何这里写的是 PI/6,而不是 30?这是因为 Processing 中规定,sin 函数中传入的参数采取的是弧度制。

所谓的弧度制,就是用单位圆的弧度长来表示角度。

假设单位圆的半径为 1。根据周长公式 l = 2 π r, 那圆的周长就是 2 π。而 2 π 的弧长(周长),就对应 360 度。同理,一个角的角度如果是 30 度。那它对应的弧长(图中红线部分),就为整个圆弧长十二分之一,即 2 * π / 12,为 PI / 6。

所以程序中写 sin(PI/6) ,意思就是求 30 度的 sin 值是多少。

当然,如果你更习惯用角度制来表示。程序中还可以这么写,只要使用函数 radians,它就能自动将角度值换算成弧度值,相当方便。

println(sin(radians(30)));

输出结果与原来一样,都为 0.5。

通过图像中认识三角函数

前面铺垫完基础知识,下面就进入正题,开始在程序中作画。首先,我们需要把三角函数的函数曲线画出来

绘制函数曲线

例子01:

PVector posA, posB; void setup() {  size(700, 400);  background(255);  posA = new PVector(0, 0);  posB = new PVector(0, 0); } void draw() {  float speed = 0.02;  posA.x++;  posA.y = height/3 + sin(posA.x * speed) * 40;  posB.x++;  posB.y = height/3 * 2 + cos(posA.x * speed) * 40;  strokeWeight(5);  point(posA.x, posA.y);  point(posB.x, posB.y); }

代码浅析:

  • 例子中为了方便对比,同时绘制了 sin 和 cos 函数的图像。其中 PVector 代表向量,常用用于表示点坐标

  • A ,B 两点的横坐标都不断递增,这个递增值作为自变量,传入到 sin 和 cos 函数中便获得输出值,接着把输出值的变化,赋值到 A,B 两点的纵坐标上

  • background 写在 setup 函数中,会使得 A,B 两点移动的轨迹保留在画布上

  • 从上图可以发现,sin 和 cos 的图形呈现周期变化。图形的大小比例完全相同的,只是在 x 轴方向的位置有所不同。

接下来,我们试着在上例的基础上,把 speed 的值改成 0.2

又或是改得非常小,写成 0.005;

对比原图,会发现图形被拉伸或是压缩。这是由于输入值的变化速度发生改变了。输入值变化越慢,输出值也会越慢,因而起伏越小。

如果在绘制的时候,我们还想严格地控制“波峰”“波谷”的数量。那就需要注意输入值的变化范围。

由于 sin 函数和 cos 函数,他们的周期都为 2 π。所以当输入值的范围恰好是 2 π 的整数倍时,就可以在屏幕上绘制出与倍数相同的“波峰”“波谷”,并且是可以首尾衔接的。

试着将 speed 写成

float speed = 2 * PI * 0.01;

因为屏幕宽为 700,所以恰好就能绘制出 7 段重复的波形,可以代入到例子中仔细思考其中的含义。

三角函数的简单应用

在程序中使用三角函数,基本就是靠这个输出值来做文章。因为 sin 和 cos 函数是可以互相转换的,所以在具体应用时,使用 sin 还是 cos 并没有太大差别。

现在,试着把 sin 值的变化,映射到圆的半径,就能产生这样的效果

例子02:

void setup() {  size(700, 400); } void draw(){  background(255);  float l = sin(frameCount/100.0) * 200;  fill(0);  ellipse(width/2,height/2,l,l); }

映射到角度

例子03:

void setup() {  size(700, 400); } void draw(){  background(255);  float angle = sin(frameCount/100.0) * PI/2;  translate(width/2,height/2);  rotate(angle);  rectMode(CENTER);  fill(0);  rect(0,0,200,200); }

映射到圆的位移,利用输入值的差异产生错落效果

例子04:

void setup() {  size(700, 400); } void draw() {  background(255);  fill(0);  for (int i = 0; i <= 7; i++) {    float h = sin(i/2.0 + frameCount/10.0) * 100;    ellipse(i * 100, 200 + h, 80, 80);  } }

从上面三个小实例可以看出,若想精确地控制图形元素,就要熟悉它的周期性,控制它的输入输出范围。位移的多少,角度的变化快慢,都与之息息相关。

使用三角函数实现圆周运动

三角函数的一个重用应用,就是使用它画圆

例05:

PVector pos; void setup(){  pos = new PVector(); } void draw(){  background(255);  float r = 180;  pos.x = r * cos(frameCount/100.0);  pos.y = r * sin(frameCount/100.0);  translate(width/2,height/2);  fill(0);  ellipse(pos.x,pos.y,40,40); }

具体原理可以查看下图,它可以根据三角函数的定义推导得出。想象在圆周上有一个黑色圆点在运动,它构成的直角三角形的两个直角边,就对应它的坐标值。

而从中围成的三角形,由于

cos(α) = x / r sin(α) = y / r

很容易能推出

x = r * cos(α)
y = r * sin(α)

所以我们只要在 α 处,传入一个不断自增的参数。x,y 的坐标就会输出圆周运动。

网上有其他朋友制作了这样一张动图,非常形象直观地揭示了底层原理

巧妙!

综合应用

前面介绍的圆周运动,经过特殊组合,可以“编织”出一些非常有意思的图案

例06:

PVector posA,posB; void setup(){  size(700,700);  background(0);  posA = new PVector();  posB = new PVector(); } void draw(){  float r1 = 300;  float speed1 = frameCount/100.0 * 4;  posA.x = r1 * cos(speed1);  posA.y = r1 * sin(speed1);  float r2 = 150;  float speed2 = frameCount/100.0;  posB.x = r2 * cos(speed2);  posB.y = r2 * sin(speed2);  translate(width/2,height/2);  stroke(255,100);  line(posA.x,posA.y,posB.x,posB.y); }

代码浅析:

  • 这里设定了两个做圆周运动的点,并对它们进行连线。由于两点的运动半径以及运动速度都不一致,所以就使得直线在交织时,产生深浅不一的效果

试着改变传入的速度,半径

也可以开启叠加模式,实时改变线条的颜色

例07:

PVector posA,posB; void setup(){  size(700,400);  background(0);  posA = new PVector();  posB = new PVector(); } void draw(){  float r1 = 300;  float speed1 = frameCount/300.0 * 4;  posA.x = r1 * cos(speed1);  posA.y = r1 * sin(speed1);  float r2 = 150;  float speed2 = frameCount/300.0;  posB.x = r2 * cos(speed2);  posB.y = r2 * sin(speed2);  translate(width/2,height/2);  colorMode(HSB);  blendMode(ADD);  stroke(frameCount/10.0 % 255,255,255,30);  line(posA.x,posA.y,posB.x,posB.y); }

下面的图片都是在同一个结构中,修改不同参数产生的。只是画布设置得更大,同时让线条的运动变得更平缓,这样就能产生丰富细腻的层次变化

利用 sin 函数实现动态画笔

接下来再分享两个有关画笔的实例,这节的介绍就快到尾声

先看基础版

例08:

ArrayList<PVector> brushLines = new ArrayList<PVector>(); void setup() {  size(700,700); } void draw() {  background(0);  for (int i = 0; i < brushLines.size(); i++) {    PVector tempPos = brushLines.get(i);    float l = sin(frameCount/20.0 + i/4.0) * 50;    ellipse(tempPos.x,tempPos.y,l,l);  } } void mouseDragged() {  brushLines.add(new PVector(mouseX, mouseY)); }

拖动鼠标,就会记录绘制轨迹。sin 函数此时会控制圆点的半径。

下面再看加强版

例09:

ArrayList<PVector> brushLines = new ArrayList<PVector>(); PImage myPic; void setup() {  size(1200, 700);  myPic = loadImage("pic.jpg");  myPic.resize(width, height); } void draw() {  background(0);  noStroke();  tint(125);  image(myPic, 0, 0);  for (int i = 0; i < brushLines.size(); i++) {    PVector tempPos = brushLines.get(i);    float r = 10 * sin(i/3.0 + millis()/200.0);    fill(myPic.get(int(tempPos.x), int(tempPos.y)));    ellipse(tempPos.x, tempPos.y, r, r);  } } void mouseDragged() {  brushLines.add(new PVector(mouseX, mouseY)); } void keyPressed(){  if(key == 'c'){    brushLines.clear();  } }

拖动鼠标绘制图案,按 c 键清空画布

https://v.qq.com/txp/iframe/player.html?vid=b0172gz439i&width=500&height=375&auto=0

在运行实例前,需要先寻找一张图片素材放到所在文件夹中,来作为程序的背景图片。绘制的笔触会吸取背后的图片颜色,与经过变暗处理的背景相组合,就会产生类似于发光的效果。推荐使用梵高的作品做测试,绚丽的色彩和奔放的笔触,会更相得益彰。


如果做出满意作品,可以试着导出 Gif (Processing3.0 Gif动图导出技巧

三角函数其他应用

三角函数还有其他用法吗?非常多。最后再截选一些之前的习作,它们都有用到三角函数。

(控制关节的旋转角度,模拟翅膀摆动效果)

(使用三角函数确定关节坐标)

(已知方向和步长,计算下个生长的坐标点)

END

这节有关三角函数的介绍就到这里。希望接下来和它接触的日子里,你会和我一样爱不释手~


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

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