前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Processing之完美循环的艺术

Processing之完美循环的艺术

作者头像
ChildhoodAndy
发布2021-11-09 10:34:00
1.8K0
发布2021-11-09 10:34:00
举报
文章被收录于专栏:小菜与老鸟小菜与老鸟

前言

我们经常在社交网站上会看到一些生成艺术使用的视频或者 GIF 展示,不过不知道读者有没有仔细观察过有些视频和 GIF,他们的开头和结尾是无缝衔接的,或者说在某个时间点又开始重复循环。

小菜在 Tumblr 浏览时无意中翻到了作者 necessary-disorder 的作品,颇为喜欢,他的作品基本都是这种无限循环风格。

如何做出完美循环?

完美循环最重要的一点就是“将来能够在某个时刻,能够再次展现开始帧”。如果我们给开始帧画面打个标记 A,那么不管我们的动画经过怎么变化,只要中间能够再次绘制 A 帧画面,就能够实现完美循环。

Processing中的处理方式

这个就涉及到了今天小菜要给大家分享的主题,就是『完美循环 GIF 输出』。不仅仅要实现完美循环,还要输出成 GIF。这块内容也是小菜观看 [Shiffman 的教学视频](https://www.youtube.com/watch?v=nBKwCCtWlUg) 的一个文字总结和分享,希望对大家有所帮助。

教学中提到了一个 github 开源项目,LoopTemplates[1],这个项目里面展示了如何使用 Processing Java、p5.js、Processing Python 来创建一个完美循环 GIF,算是一个模板 Template,参考这个模板,结合我们的想法,会轻松的做出完美循环 GIF 动画。

思路

代码语言:javascript
复制
// loop动画所需要的帧数
int nFramesInLoop = 120;
// 随着时间已经流逝、播放的帧数
int nElapsedFrames;
// 是否正在录制
boolean bRecording; 

void setup() {
  size (500, 200); 
  bRecording = false;
  nElapsedFrames = 0;
}

void draw() {
  // 计算loop动画进度的百分比值(0到1)
  float percentCompleteFraction = 0; 
  if (bRecording) {
    percentCompleteFraction = (float) nElapsedFrames / (float)nFramesInLoop;
  } else {
    percentCompleteFraction = (float) (frameCount % nFramesInLoop) / (float)nFramesInLoop;
  }


  // 基于这个loop进度进行绘制
  renderMyDesign (percentCompleteFraction);

  // 如果处于录制中,则保存序列帧图片
  if (bRecording) {
    saveFrame("frame_" + nf(nElapsedFrames, 4) + ".png");
    // 增加loop播放的帧数
    nElapsedFrames++; 
    // 如果达到了一个循环,则停止录制
    if (nElapsedFrames >= nFramesInLoop) {
      bRecording = false;
    }
  }
}

// 一个由 0 - 1 的小数,驱动的绘制
void renderMyDesign (float percent) {
  
}

// 按键事件
// f / F 键触发录制
void keyPressed() {
  if ((key == 'f') || (key == 'F')) {
    bRecording = true;
    nElapsedFrames = 0;
  }
}

1)当按下键盘 f 或者 F 键时,开始录制,设定bRecording布尔值为true,以及nElapsedFrames流逝的帧数归置为 0。

2)在每帧绘制的时候,我们要计算出一个 loop 动画的进度完成比,percentCompleteFraction = (float) nElapsedFrames / (float)nFramesInLoop;

3)我们根据这个动画完成比进行自定义动画实现

4)录制的时候,使用saveFrame保存帧画面成序列图到本地,同时nElapsedFrames递增1,当nElapsedFrames大于等于我们规定的一个 loop 动画帧总数,那么便停止止录制,设定bRecording = false

比如在renderMyDesign(float percent)中根据循环动画进度完成比,来实现上面 GIF 中的方块自旋和小圈围着方块中心旋转的循环逻辑:

代码语言:javascript
复制
void renderMyDesign (float percent) {
  background (180);
  smooth(); 
  stroke (0, 0, 0); 
  strokeWeight (2); 


  float cx = 100;
  float cy = 100;
  

  float radius = 80; 
  // 根据循环动画的进度计算圆旋转的角度
  float rotatingArmAngle = percent * TWO_PI;  
  float px = cx + radius*cos(rotatingArmAngle); 
  float py = cy + radius*sin(rotatingArmAngle); 
  fill    (255); 
  line    (cx, cy, px, py); 
  ellipse (px, py, 20, 20);


  pushMatrix(); 
  translate (cx, cy);
  // 根据循环动画的进度计算方块自旋的角度
  float rotatingSquareAngle =  percent * TWO_PI * -0.25;
  rotate (rotatingSquareAngle); 
  fill (255, 128); 
  rect (-40, -40, 80, 80);
  popMatrix(); 
 
  // ...
}

GIF 合成

关于 GIF 导出这块,我们有了之前的序列图,就容易多了。小菜推荐两个 GIF 制作网站,只需要把序列图上传上去,设定好动画帧速度,还可以设置循环次数(默认0为无限次),即可导出。当然,一些朋友可能习惯使用 Photoshop 来处理下,都是可以的。

  • https://gifmaker.me/[2]
  • https://ezgif.com/maker[3] Shiffman视频中提到的,但小菜个人感觉没有上面的好

gifmaker.me

有一个专门分享 loop gif 的网站[4],读者朋友感兴趣可以戳一下,很不错哦。不过有些 loop 是完美的,第一帧和最后一帧是衔接的,有些不是。

更多精彩玩法

下面的代码来自processingperfectloops/[5],这篇文章给了小菜很多启发。

不知道大家伙有没有注意到在上面的模板代码中,当bRecordingfalse的时候,计算动画完成比的公式。

代码语言:javascript
复制
if (bRecording) {
 percentCompleteFraction = (float) nElapsedFrames / (float)nFramesInLoop;
} else {
 percentCompleteFraction = (float) (frameCount % nFramesInLoop) / (float)nFramesInLoop;
}

这就是frameCount在循环动画中的作用。

1)frameCount % nFramesInLoop:除法取余操作,保证得出来的值在0 - (nFramesInLoop - 1)范围内

2)(float) (frameCount % nFramesInLoop) / (float)nFramesInLoop:上步骤1取余得到的值再除以循环帧总数,则将最后的值归一化,限定在了0 - 1之间。

我们可以将计算百分比的方式抽象成一个函数,这个函数从draw的次数也就是绘制帧的次数这个角度表达出的意思就是我希望这个循环动画在nFramesInLoop帧数中完成,每次draw的时候函数返回我动画0-1的进度。如果我们在setup中使用frameRate(value)函数设定了帧数,即一秒钟绘制的帧数,那么从时间角度来说就是我希望这个循环动画 nFramesInLoop / value 秒内完成

代码语言:javascript
复制
float timeLoop(int nFramesInLoop) {
  return (float)(frameCount % nFramesInLoop) / (float)nFramesInLoop;
}

例子1:一个从左到右在屏幕中来回运动的方块

代码语言:javascript
复制
void setup() {
  size(704, 90);
  frameRate(30);
}

void draw() {
  background(0);
  fill(255);
  rect(timeLoop(60) * width, height / 2, 40, 40);
}

float timeLoop(float nFramesInLoop) {
  return frameCount % nFramesInLoop / nFramesInLoop;
}

我们希望方块在 60 帧内,位置 x 坐标从 0 运动到 width 大小,因为帧率是 30帧/秒,也就是方块在 2 秒内从左到右完成一次动画循环。

例子2:时间错位

单个方块从左到右循环有些枯燥和乏味,如果绘制了多个方块呢?如何让多个方块之间有一种时间差的运动?也就是时间错位。

timeLoop中我们引入另外一个参数offset用来增加一个偏移量,我们来看下这个代码:

代码语言:javascript
复制
void setup() {
  size(704, 150);
  frameRate(30);
}

void draw() {
  background(0);
  fill(255);
  rect(timeLoop(60, 0) * width, 30, 30, 30);
  rect(timeLoop(60, 20) * width, 60, 30, 30);
  rect(timeLoop(60, 40) * width, 90, 30, 30);
}

float timeLoop(int nFramesInLoop, float offset) {
  return (float)((frameCount + offset) % nFramesInLoop) / (float)nFramesInLoop;
}

例子3:高度循环

一个timeLoop返回值的0-1的循环,不仅仅可以用于例子1和2中的位置的变化,也可以用于更多数值的变化,比如大小的变化,如高低长宽等。当然,这个可以用在任何想要循环的数值上。在这个例子中,我们赋予单个竖条矩形的高度的变化(从 0 到 100,然后突变到 0,继续开始从 0 到 100),然后再赋予竖条方块时间错位,形成下面的动态:

代码语言:javascript
复制
void setup() {
  size(704, 200);
  frameRate(30);
  noStroke();
}

void draw() {
  background(0);
  fill(255);
  background(0);
  for (float i = 0; i < 1; i += 1 / 16.0) {
    float barheight = timeLoop(60, i * 60) * 100;
    rect(36 + i * 640, 150 - barheight, 32, barheight);
  }
}

float timeLoop(float nFramesInLoop, float offset) {
  return (frameCount + offset) % nFramesInLoop / nFramesInLoop;
}

例子4:缓动曲线

到目前为止,所有动画都以线性方式移动——对于每一帧,移动的距离是相同的。线性计时非常机械化,也不是特别优雅。鉴于我们正在处理归一化值,可以应用缓动曲线。timeLoop的结果只需要通过所需的曲线即可。下面的函数将锯齿波(线性时序)转换为三角波。这将使我们的方块上下移动,而不仅仅是向上移动。

小菜绘制了一些原理图,帮助读者彻底理解这里的函数叠加变换过程。

下图是timeLoop函数随着frameCount的递增,它的函数图形,可以看到图形是一个锯齿波形,从 0 到 1 后,又重新从 0 到 1,两个周期的值不是衔接的,从 1 跳变为 0。

那我们来看下tri函数

代码语言:javascript
复制
float tri(float t) {
  return t < 0.5 ? t * 2 : 2 - (t * 2);
}

tri函数的入参取值范围是0-1的值,在0-0.5区间,实现了函数返回值0-1的变化,而在0.5-1区间,实现了函数返回值1-0的变换。

那么将两个函数叠加起来呢?也就是nFramesInLoop作为timeLoop的入参,timeLoop的返回值又作为tri函数的入参,形式如

代码语言:javascript
复制
tri(timeLoop(nFramesInLoop))

两个函数的叠加,完美实现了在时间维度(frameCount)上的0-1,1-0的线型返回通道。

我们再看下inOutSin函数:

代码语言:javascript
复制
float inOutSin(float t) {
  return 0.5 - cos(PI * t) / 2;
}

inOutSin的入参是0-1,它的返回值在0-1范围内,一个非常完美的函数,实现了0-1区间的输入和0-1区间的输出这样一个正弦曲线。

我们玩点大的,将三个函数叠加到一起,inOutSin(tri(timeLoop(nFramesInLoop)))

哇塞,真是完美,完美实现了在时间维度(frameCount)上,输出值的0-1, 1-0的正弦缓动变化。

所以总结下,timeLoop和缓动函数都是标准化的,它们可以按任何顺序组合。下面的更改采用timeLoop的结果,使其成为三角波,然后使其具有缓入缓出正弦时序。

代码语言:javascript
复制
void setup() {
  size(704, 200, P2D);
  frameRate(30);
  noStroke();
}

void draw() {
  background(0);
  for (float i = 0; i < 1; i += 1 / 16.0) {
    float barheight = inOutSin(tri(timeLoop(60, i * 60))) * 100;
    rect(36 + i * 640, 150 - barheight, 32, barheight);
  }
}

float timeLoop(float nFramesInLoop, float offset) {
  return (frameCount + offset) % nFramesInLoop / nFramesInLoop;
}

float tri(float t) {
  return t < 0.5 ? t * 2 : 2 - (t * 2);
}

float inOutSin(float t) {
  return 0.5 - cos(PI * t) / 2;
}

例子5:更多的变化

如果我们再添加两个重复的竖条,添加一些颜色,并将绘制的混合模式设置为“添加”,画面看起来就更加丰富了!

代码语言:javascript
复制
void draw() {
  background(0);
  blendMode(ADD);
  float barheight = 0;
  for (float i = 0; i < 1; i += 1 / 16.0) {
    fill(#ff0000);
    barheight = inOutSin(tri(timeLoop(60, i * 60))) * 100;
    rect(36 + i * 640, 150 - barheight, 32, barheight);
    fill(#00ff00);
    barheight = inOutSin(tri(timeLoop(60, i * 60 + 20))) * 100;
    rect(36 + i * 640, 150 - barheight, 32, barheight);
    fill(#0000ff);
    barheight = inOutSin(tri(timeLoop(60, i * 60 + 40))) * 100;
    rect(36 + i * 640, 150 - barheight, 32, barheight);
  }
}

结尾

最后来个轻松点的无限循环,套娃模式

参考资料

[1]

LoopTemplates: https://github.com/golanlevin/LoopTemplates

[2]

https://gifmaker.me/: https://gifmaker.me/

[3]

https://ezgif.com/maker: https://ezgif.com/maker

[4]

专门分享 loop gif 的网站: https://giphy.com/explore/perfect-loop

[5]

processingperfectloops/: https://bjango.com/articles/processingperfectloops/

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-10-31,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 小菜与老鸟 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 如何做出完美循环?
  • Processing中的处理方式
    • 思路
    • GIF 合成
    • 更多精彩玩法
      • 例子1:一个从左到右在屏幕中来回运动的方块
        • 我们希望方块在 60 帧内,位置 x 坐标从 0 运动到 width 大小,因为帧率是 30帧/秒,也就是方块在 2 秒内从左到右完成一次动画循环。
          • 例子2:时间错位
            • 例子3:高度循环
              • 例子4:缓动曲线
                • 例子5:更多的变化
                • 结尾
                  • 参考资料
                  领券
                  问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档