像Discord登录页面中的“Wobbly Canvas”一样制作动画?

10

参考一下,我说的是在Discord登录页面左上方的深灰色空间。无法访问该链接的人,这里有一个截图:

登录页面截图

它有很多非常酷的效果,小圆点和(更暗的阴影)随着鼠标移动,但我更感兴趣的是“摇晃边缘”效果,稍微次之的是“快速晃动/缩放”页面加载(在加载时缩放画布可以产生类似但不一定优美的效果)。

不幸的是,我不能提供太多关于最小可重现示例的内容,因为我不确定从哪里开始。我尝试查看Discord的资源,但我对Webpack不熟悉,无法确定发生了什么。

我能够挖掘出的有关“动画波浪/摆动”的所有信息都是由CSS驱动的SVG或剪辑路径边框,我想制作出更有机的东西。


我不是CSS/Canvas的专家,但我熟悉Webpack。如果你感兴趣的话,我可以指引你到Discord源代码的正确位置。 - hackape
2个回答

21

非常有趣的问题。我已经把这个blob缩小了,使其在下面的预览中可见。

这里还有一个CodePen,是更大的尺寸。

const SCALE = 0.25;
const TWO_PI = Math.PI * 2;
const HALF_PI = Math.PI / 2;
const canvas = document.createElement("canvas");
const c = canvas.getContext("2d");

canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
document.body.appendChild(canvas);

class Blob {
  constructor() {
    this.wobbleIncrement = 0;
    // use this to change the size of the blob
    this.radius = 500;
    // think of this as detail level
    // number of conections in the `bezierSkin`
    this.segments = 12;
    this.step = HALF_PI / this.segments;
    this.anchors = [];
    this.radii = [];
    this.thetaOff = [];

    const bumpRadius = 100;
    const halfBumpRadius = bumpRadius / 2;

    for (let i = 0; i < this.segments + 2; i++) {
      this.anchors.push(0, 0);
      this.radii.push(Math.random() * bumpRadius - halfBumpRadius);
      this.thetaOff.push(Math.random() * TWO_PI);
    }

    this.theta = 0;
    this.thetaRamp = 0;
    this.thetaRampDest = 12;
    this.rampDamp = 25;
  }
  update() {
    this.thetaRamp += (this.thetaRampDest - this.thetaRamp) / this.rampDamp;
    this.theta += 0.03;

    this.anchors = [0, this.radius];
    for (let i = 0; i <= this.segments + 2; i++) {
      const sine = Math.sin(this.thetaOff[i] + this.theta + this.thetaRamp);
      const rad = this.radius + this.radii[i] * sine;
      const theta = this.step * i;
      const x = rad * Math.sin(theta);
      const y = rad * Math.cos(theta);
      this.anchors.push(x, y);
    }

    c.save();
    c.translate(-10, -10);
    c.scale(SCALE, SCALE);
    c.fillStyle = "blue";
    c.beginPath();
    c.moveTo(0, 0);
    bezierSkin(this.anchors, false);
    c.lineTo(0, 0);
    c.fill();
    c.restore();
  }
}

const blob = new Blob();

function loop() {
  c.clearRect(0, 0, canvas.width, canvas.height);
  blob.update();
  window.requestAnimationFrame(loop);
}
loop();

// array of xy coords, closed boolean
function bezierSkin(bez, closed = true) {
  const avg = calcAvgs(bez);
  const leng = bez.length;

  if (closed) {
    c.moveTo(avg[0], avg[1]);
    for (let i = 2; i < leng; i += 2) {
      let n = i + 1;
      c.quadraticCurveTo(bez[i], bez[n], avg[i], avg[n]);
    }
    c.quadraticCurveTo(bez[0], bez[1], avg[0], avg[1]);
  } else {
    c.moveTo(bez[0], bez[1]);
    c.lineTo(avg[0], avg[1]);
    for (let i = 2; i < leng - 2; i += 2) {
      let n = i + 1;
      c.quadraticCurveTo(bez[i], bez[n], avg[i], avg[n]);
    }
    c.lineTo(bez[leng - 2], bez[leng - 1]);
  }
}

// create anchor points by averaging the control points
function calcAvgs(p) {
  const avg = [];
  const leng = p.length;
  let prev;

  for (let i = 2; i < leng; i++) {
    prev = i - 2;
    avg.push((p[prev] + p[i]) / 2);
  }
  // close
  avg.push((p[0] + p[leng - 2]) / 2, (p[1] + p[leng - 1]) / 2);
  return avg;
}

这里涉及到很多内容。如果想要创建此效果,你需要对二次贝塞尔曲线如何定义有良好的工作知识。一旦掌握了这个知识,有一个老技巧我多年来用过很多次,可以生成平滑连接的二次贝塞尔曲线,即定义一个点列表并计算它们的平均值,然后使用这些点作为控制点,新的平均点作为锚点。请参阅bezierSkincalcAvgs函数。

拥有绘制平滑贝塞尔曲线的能力,其余的就是将点定位在弧形上并对它们进行动画处理。为此,我们使用一些数学:

x = radius * sin(theta)
y = radius * cos(theta)

这将极坐标转换为笛卡尔坐标。其中theta是圆周上的角度[0-2pi]

至于动画,这里还有很多事情要做 - 我会尽力在本周末更新答案,提供更多细节和信息,但希望这对你有所帮助。


2
这非常有帮助,正是我在寻找的!非常感谢您抽出时间进行解释,这非常有帮助,超出了我的期望! - Xhynk
很高兴能帮忙:D Xhynk - Zevan
2
这正是我为什么喜欢Stackoverflow的原因。这就是艺术。 - masoudmanson
@Zevan谢谢你分享这段Codepen代码。我需要改变什么来让这个blob覆盖整个窗口? - Niklas
1
@AmooHesam 很酷!感谢你做到了那个:D 看起来很棒! - Zevan
显示剩余4条评论

8
动画在画布上运行,是一个简单的贝塞尔曲线动画。
为了获得有机的感觉,您应该查看Perlin噪声,这是在开发原始Tron视频FX时引入的。
您可以在这里找到一个很好的指南来理解perlin噪声。
在示例中,我使用了https://github.com/josephg/noisejs。

var c = $('canvas').get(0).getContext('2d');
var simplex = new SimplexNoise();
var t = 0;

function init() {
 window.requestAnimationFrame(draw);
}

function draw() {
c.clearRect(0, 0, 600, 300);
c.strokeStyle="blue"; 
c.moveTo(100,100);
c.lineTo(300,100);
c.stroke();                

// Draw a Bézier curve by using the same line cooridinates.
c.beginPath();              
c.lineWidth="3";
c.strokeStyle="black"; 
c.moveTo(100,100);
c.bezierCurveTo((simplex.noise2D(t,t)+1)*200,(simplex.noise2D(t,t)+1)*200,(simplex.noise2D(t,t)+1)*200,0,300,100);
c.stroke();

// draw reference points
c.fillRect(100-5,100-5,10,10);
c.fillRect(200-5,200-5,10,10);
c.fillRect(200-5,0-5,10,10);
c.fillRect(300-5,100-5,10,10);
t+=0.001;
window.requestAnimationFrame(draw);
}

init();
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/simplex-noise/2.4.0/simplex-noise.js"></script>
<canvas width="600" height="300"></canvas>

注意:在进一步研究Discord源代码时,我指出它正在使用https://www.npm.red/~epistemex 库。Epistemex的NPM包仍然在线上,而GitHub库和个人资料已经不存在了。

注意2:另一种方法是依赖于物理库,比如这个演示,但如果你只需要单个效果,这可能有些过头了。


1
这是一个很棒的库!感谢演示,它真的很有帮助! - Xhynk

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接