最近听见一种说法:“雷神”分为两种,一种存在于漫威宇宙,一种存在于宝可梦宇宙。前者身形高大,能抵挡一切病毒、辐射和毒气攻击,手持“雷神之锤”还可操纵雷电风暴天气。后者是一只黄皮电耗子,体重超标,容易感冒,疑似靠脸吃饭,放电方式成迷。

最近大家肯定都被下面这个靠脸吃饭的黄皮耗子萌到了吧!

 

是滴,这就是最近大火的电影《大侦探皮卡丘》里面的那一只,让人不禁感叹:原来你是这样的皮卡丘。

 

 

但今天我们的主角不是皮卡丘。大家都知道这部电影是改编自游戏《精灵宝可梦》,里面有各种各样的精灵,而皮卡丘只是其中一只。今天我们的主角是一只比较特殊的精灵——百变怪。

 

 

这次它站c位的原因不是它有变成任何精灵这种无与伦比的能力,而是它碰巧出现在了下面的这个作品中。

 

 

观察作品,可以发现一个个字母在看上去好像在这只黑黑的百变怪身上有规律的跳舞,很有律动感。

 

Circular Hello Curves:

https://www.openprocessing.org/sketch/697891

是大佬Vamoss用P5.js写的,我用Processing实现了相同的效果

 

在讲解完这个作品后,我会给大家看看我在次基础上进行的拓展——Broken Heart,大家可以想一想到底是看见可什么伤心的消息,才让一颗漂亮的心仿佛哭泣状?

 

 

心急的同学可以看看在线效果:

BrokenHeart(https://www.openprocessing.org/sketch/720376)

代码:(github.com/pearmini/processing/blob/master/CircularHelloCurves/BrokenHeart/BrokenHeart.pde)

 

 

每当我们想在Processing中显示图形,我们必须指定具体的像素位置,也就是一系列的(x,y)坐标。这个坐标系我们称之为笛卡尔坐标系——以法国数学家笛卡尔命名。

 

另一种比较有用的坐标系叫极坐标系,它用一个夹角和距离表示空间中的一个点。我们以向量为例:

- 笛卡尔坐标系:向量的x分量和y分量

- 极坐标系:向量的大小r(长度)和方向ρ(夹角)

 

 

 

Processing的绘图函数不能理解极坐标系,当我们要画图的时候,必须指定笛卡尔坐标系的位置。但是,有时候用极坐标系设计模型会更加方便。

 

幸运的是,通过三角函数可以完成极坐标系和笛卡尔坐标系的相互转换,这样我们就可以用极坐标系坐标系设计模型,然后用笛卡尔坐标系绘制图形。

 

根据上图我们可以很容易地得到两者间的转化公式,如下图:

 

 

 

下面我们从两个例子来进一步了解极坐标系。

 

现在考虑一个圆心在坐标原点,半径为r的圆。

 

在笛卡尔坐标系下,该圆的方程为 。在极坐标系下,该圆的方程如ρ=r。对于圆上任意一点(ρ,θ)来说,ρ是该点到圆心的距离,θ是从起始边转过的角度。

 

分析圆的极坐标方程,其实也不难理解它为啥是这样。因为圆的定义就是到定点的距离等于定长的点的集合,即不管θ是多少,ρ都等于半径。

 

心型线的极坐标方程很简单:ρ=r(1+cosθ),它的图像如下图所示:

 

据说这个公式出现在52岁的笛卡尔写给18岁的瑞典公主克里斯汀的一封情书,想了解更多的小伙伴可以去自行查阅这个凄美的爱情故事。

 

 

下面来讲解Circular Hello Curves所需要用到的最核心的地方:如何获得一系列周期性变化的noise随机数,并且它们没有对称性。

 

我们知道Processing中的noise函数的返回值是完全由输入参数决定,所以如果我们希望返回值是周期性的,那们输入的参数就需要是周期性的。我们不妨假设参数的周期是2π,那么很容易就会想到使用三角函数。

 

假如我们设定输入的参数序列为如下所示,那么两个周期内的图像如下。分析可以发现,虽然现在获得这一系列noise随机数是以2π为周期了,但是却有对称性。这是因为正弦函数本身就有对称轴。

 

float y = noise(sin(x) + 1);

 

 

需要说一句,将正弦函数加一的目的是让输入参数都大于零。因为noise返回的值都是在0到1之间的,所以为了保持输入参数的周期,将输入参数都变成正数。

 

为了使得返回序列没有对称性,将我们输入的参数序列改为如下所示,那么两个周期内的图像将如下。分析可以发现,现在这一系列noise随机数就满足我们的要求了。这是因为,正弦函数和余弦函数的周期都是2π,同时它们的对称轴都不相同。

 

float y = noise(sin(x) + 1, cos(x) + 1);

 

 

 

在作品Circular Hello Curves中出现了"Circular Hello Curves",所以我们先来大概了解一下如何在Processing中绘制文字。

 

在Processing中绘制文字的很简单,主要有以下几个步骤:

 

1.声明一个类型为PFfont的对象

 

 

PFont f;

 

2.创建一个字体

 

创建一个字体有如下两个方法。

 

第一种方法是使用系统自带的字体,用createFont函数创建。该函数主要需要两个参数,第一个是字体的名字,第二个是字体的大小(createFont函数更多用法(https://processing.org/reference/createFont_.html))。

 

同时可用通过PFont.list函数查看该系统可以使用的字体。

 

f = createFont("Georgia", 24);// 创建系统自带字体

println(PFont.list()); //获取所有系统可以使用的字体

 

 

如果系统自带的字体不能满足大家,并且希望自己的代码在任何平台运行的效果都一样,那么就可以用第二种方法手动创建字体。

 

在Processing自带的开发环境中选择:工具->创建字体,打开如下的窗口。然后选择想要的字体,指定大小,文件名点击ok就行了。这个时候Processing开发环境会在当前项目的根目录下创建一个名叫data的文件件,然后里面有一个扩展名为.vlw的字体文件。

 

 

接下来在setup函数中调用loadFont函数加载自己创建的字体文件。需要注意的是因为Processing从计算机内存里面加载字体文件,这个过程很慢,所以不建议在draw函数中多次调用loadFont函数。

 

 

f = loadFont("data/Serif-48.vlw");

 

3.用函数textFont规定使用字体,该函数接受两个参数

 

第一个参数是指定的字体类型,第二个参数是字体的大小。第二个参数是一个可选的参数,默认是加载字体本来的大小。

 

需要注意的是如果大家使用的是loadFont函数加载的字体,那么指定的字体大小和字体默认的大小不一致的话,可能导致绘制的文字质量不好,因为vlw格式的字体文件使用图片来展示每一个字母。但是如果大家使用的是createFont函数,就没有这个问题。

 

textFont(f, 36);

 

 

4.用fill规定使用颜色

 

fill(0);

 

第五步,用text函数绘制字体,该函数接受三个参数。第一个参数是需要绘制的文字内容,第二个和第三个参数分别是这个文字的绘制位置的x和y坐标。

 

text("hello world", 10, 100);

 

 

接下来介绍两个Processing中和绘制字体有关的函数。

 

第一个是textAlign,该函数用于指定字体的对齐方式,它只需要一个参数:CENTER、LEFT、RIGHT。大家可以通过下面这张图片来看一下这三种对齐方式的区别。

 

 

第二个是textWidth,该函数计算并返回字符串或者字符的宽度。

 

String str = "hello world";

char c = 'A';

textWidth(str); // 64.78711

textWidth(c); // 8.289297

 

现在我们就来介绍如何实现Circular Hello Curves。

 

1.我们来看一下代码的主要流程

 

首先我们声明一个类型为Shape的全局变量,这个就是百变怪和字母的混合体。然后在setup函数对其进行声明,传入的参数分别是这个混合体中心的坐标和需要绘制的文字内容。接下在draw函数中对这个混合体不断得进行刷新和绘制。最后监听鼠标移动事件,在鼠标进行移动的时候对字体进行旋转。

 

Shape s;

void setup() {

  size(600, 600);

  s = new Shape(width / 2, height /2, 100, "Circular Hello Curves");

}  

 

void draw() {

  background(255);

  s.update();

  s.display();

}

 

void mouseMoved() {

  s.rotateText();

}

 

class Shape{}

 

2.我们来看看Shape对象的构造函数,看一看这个混合体由什么构成,需要初始化什么东西。

 

分析下面的构造函数我们可以得到如下的发现。

 

百变怪是由一系列点围成的,每一个点由它的极坐标表示,而它所在的极坐标系是以Shape的x,y为坐标原点的。用一个PVector存储每一个点,其中x维度代表的是该点的方向,y维度代表的是该点的长度。初始化的时候在$[0, 2\pi)$的范围内给每一个点设置一个方向。

 

然后给每一个字符分配一个PVector,用来存储它的极坐标以及它旋转的角度,其中x维度代表的是方向,y维度代表的是长度,z维度是旋转的角度。初始化的时候只用初始化每一个字符的方向。假设该字符所占用的弧长近似等于它的宽度,根据弧度制角度的计算公式,可以计算出它对应的夹角,从而依次计算每个字符的方向。

 

  Shape(float _x, float _y, float _r, String _text) {

    x = _x;

    y = _y;

    r = _r; 

    text = _text;

 

//初始化构成百变怪的点。

    points = new ArrayList();

    for (int i = 0; i < SEGMENT; i++) {

      float theta = map(i, 0, SEGMENT - 1, 0, TAU);

      

      //初始化每一个的方向

      points.add(new PVector(theta, 0));

    }

 

//初始化需要用到的字体,并进行设置。

    f = createFont("Helvetica", _r / 2 * 1.5);

    textFont(f);

    textAlign(CENTER);

    

//初始化需要展现的字母。

    chars = new ArrayList(); 

    float theta = 0;

    for (int i = 0; i < text.length(); i++) {

      char currentChar = text.charAt(i);

      float thetaStep = textWidth(currentChar) / (_r * 1.5);

      theta += thetaStep / 2;

      

      //初始化每一个的方向

      chars.add(new PVector(theta, 0, 0));

      theta += thetaStep / 2;

    }

  }

 

然后我们再看看Shape的成员变量的声明。

 

class Shape{

  //构成百变怪点的数量

  final int SEGMENT = 200;

 

  //百变怪的扭曲程度

  final float NOISE_SCALE = 1;

  ArrayList<PVector> points;

  ArrayList<PVector> chars;

  

  float x, y, r;

 

  //绘制字体的启始角度

  float startAngle;

  String text;

  PFont f;

  //...

}

 

3.update

 

在每一次update的时候,我们会更新每一个点的位置,这个主要是更新它到原点的距离来实现的。同时也会更新每一个字符的位置和旋转角度,每个字符的位置也是通过更新它到原点的距离实现的。

 

  void update() {

 

    for (PVector p : points) {

      p.y = rByTheta(p.x);

    }

 

    for (PVector c : chars) {

      c.y = rByTheta(c.x + startAngle);

      c.z = angleByTheta(c.x + startAngle);

    }

  }

 

下面我们首先来看一下每一个点如何根据它的方向来确定它到原点的距离。我们会对r乘上一个scale来达到扭曲效果,而scale就是我们前面提到的周期性随机数。不同的是,我们传入了第三个参数,使得每一次刷新的时候,每一个点的到原点的距离发生变化,这样才会有膨胀和收缩的效果。

 

  float rByTheta(float theta) {

    float time = float(frameCount) / 100;

    float scale = noise(cos(theta) + 1, sin(theta) + 1, time) * NOISE_SCALE + 1;

    return  r * scale;

  }

 

然后我们来分析如何通过该点的方向来确定每个字符的旋转角度。我们首先获得该字符附近左右的两个点,然后根据这两个点到原点的距离获得该字符的旋转角度。具体方法如代码所示。

 

  float angleByTheta(float theta) {

    final float offset = 0.06;

    float theta1 = theta - offset, theta2 = theta + offset;

    float a = rByTheta(theta1);

    float b = rByTheta(theta2);

    return atan2(a * sin(theta1) - b * sin(theta2), a * cos(theta1) - b * cos(theta2)) + PI;

  }

 

在该部分的最后我们再来看如何实现文字随着鼠标旋转的效果。我们是通过更新文字的起始角度来实现的,而该角度是鼠标与坐标原点的连线和起始边的夹角的角度,我们通过如下的方式计算而得。

 

  void rotateText() {

    startAngle = atan2(mouseY - y, mouseX - x);

  }

 

4.最后来我们来看一下Shape函数,看它是如何绘制这个混合体的。

 

首先将points中所有点绘制并连接起来,然后填充为黑色。这样就将混合体中的百变怪部分绘制完成了。

 

接下来就是根据chars里面存储的数据对每一个字符进行绘制。值得注意的是每个字符保存的方向是相对于起始角度的,所以在绘制的时候应该加上startAngle。

 

  void display() {

    //绘制百变怪

    translate(x, y);

    beginShape();

    fill(0);

    for (PVector p : points) {

      vertex(p.y * cos(p.x), p.y * sin(p.x));

    }

    endShape();

 

    //绘制文字

    for (int i = 0; i < text.length(); i++) {

      PVector c = chars.get(i);

      pushMatrix();

      translate(c.y * cos(c.x + startAngle), c.y * sin(c.x + startAngle));

      rotate(c.z);

      text(text.charAt(i), 0, 0);

      popMatrix();

    }

  }

 
 

接下来给大家简单讲解一下我在Circular Hello Curves基础上实现的Broken Heart。主要分为两步,第一步将扭曲前的图形从圆形变成心形,第二步添加一双随着鼠标转动的眼睛。

 

1.变成心形

 

从圆形变成心形很简单,用我们之前提到的心型线就ok,修改rByTheta如下。这里改变余弦函数初相的目的是将心型线顺势针旋转九十度,使得心尖向下。

 

  float rByTheta(float theta) {

    float time = float(frameCount) / 100;

    float scale = noise(cos(theta) + 1, sin(theta) + 1, time) * NOISE_SCALE + 1;

    return  r * (1 + cos(theta - PI / 2)) * scale ;

  }

 

2.添加眼睛

 

添加眼睛我们需要对Shape这个类做如下的修改。最主要的是增加了一个rotateEye的函数,该函数的作用是让眼睛跟着鼠标转动。

 

class Shape{

//...

Eye left, right;

 

Shape(float _x, float _y, float _r, String _text){

  //...

  left = new Eye(-r / 1.8, r, r );

      right = new Eye(r / 1.8, r, r);

}

 

void rotateEye() {

  left.update(mouseX - x, mouseY - y);

  right.update(mouseX - x, mouseY - y);

  }

  

void display(){

  //draw Eyes

      left.display();

      right.display();

}

//...

}

 

接下来我们将监听鼠标移动事件函数中修改如下。

 

void mouseMoved() {

  s.rotateText();

  s.rotateEye();

}

 

同时也修改传入Shape的位置和需要绘制的文字。

 

s = new Shape(width / 2, height / 3, 100, "Sad news!!!");

 

 

最后我们来看一下Eye这个类的定义,绘制一个眼睛的时候会绘制两个圆,分别代表眼白和眼珠,其中眼珠的方向是朝着鼠标的方向。

 

class Eye {

  float x, y, size, angle;

  Eye(float _x, float _y, float _size) {

    x = _x;

    y = _y;

    size = _size;

  }

 

  void update(float mx, float my) {

    angle = atan2(my - y, mx - x);

  }

 

  void display() {

    pushMatrix();

    translate(x, y);

    fill(255);

    ellipse(0, 0, size, size);

    rotate(angle);

    fill(0);

    ellipse(size/4, 0, size/2, size/2);

    popMatrix();

  }

}

 

代码部分就讲解完了,下面再给大家说两个我能想到的继续拓展的方向。

 

泪汪汪的眼睛:Broken Heart的眼睛只是简单的圆形,说句实话,看上去不太像伤心的样子。如果我们能用上面的方法把眼睛扭曲一下,增加一种泪汪汪的感觉,是不是能看上去更悲痛欲绝?

 

更多的形状:我们目前只是在圆形和心形基础上进行扭曲,大家可以去找更多的方程,将它们的图像进行扭曲,可能会得到一些更加炫酷的效果!

 

那么又到了和大家说再见的时候了。今天我们一起看了字母在百变怪上翩翩起舞,也看了一颗扭曲到变形的伤心的心。

 

使用Processing制造翻版宠物小精灵是不是很有趣呢,现在Processing基本已经成为设计师与艺术家行业的新宠!使用它可以快速实现自己的交互创意想法,还可以与硬件进行体感互动、人脸识别、语音控制等。