用zdog创建动画鸡
#javascript #前端 #showdev #3d

演示:https://rotating-chicken.vercel.app/
代码:https://codesandbox.io/s/rotating-chicken-pcj9w1

前一段时间,我找到了zdog-这是一个轻巧的3D JavaScript引擎,用于帆布或SVG。如果您查看其网站,您会发现它被描述为 pseudo 3D 。这意味着什么?基本上,ZDOG拥有3D形状的型号,但将所有内容呈现为2D平面形状。

不是什么

您应该预先知道ZDOG有其局限性。没有碰撞检测或物理,没有梯度,没有透视或摄像头,没有照明以及您可能想要的其他一些东西。如果您需要的话,您可能想查看更强大和更实用的3D引擎,例如three-js

这是什么

听起来很有限吗?但是,在这些约束中,您实际上可以使用ZDOG做一些非常了不起的事情,而无需三个JS固有的所有开销和复杂性。只需看看demos page即可 - 有很多整洁的例子,说明了哪个Zdog最好的:一种平坦的,圆形的美学,具有简单的形状。这使其非常适合设计师。

这也意味着使事物实际上看起来不错非常容易。不再想弄清楚您是否应该使用MeshLambertMaterialMeshPhongMaterial,或者在何处将点灯或区域灯与场景相关。只需创建形状并给它颜色。

另外,圆形形状是实际的圆圈 - 这意味着您在真正的3D中都不会有多边形锯齿状。

使用方便

您会发现ZDOG IS 确实易于使用。它具有声明性的API,可让您描述所需的形状,并将它们添加到场景中。文档中的示例描述了这一点。

首先,您添加画布或SVG:

<canvas class="zdog-canvas" width="240" height="240"></canvas>

然后创建形状并将它们添加到场景中:

// create illustration
let illo = new Zdog.Illustration({
  // set canvas with selector
  element: '.zdog-canvas',
});

// add circle
new Zdog.Ellipse({
  addTo: illo,
  diameter: 80,
  stroke: 20,
  color: '#636',
});

// update & render
illo.updateRenderGraph();

所以让我们去鸡。我不会包含所有代码(您可以在沙箱中找到它),但我将包括主要零件。首先,我们需要弄清楚我们需要哪些单独的形状。在这种情况下,它看起来像这样:

diagram of the pieces of a chicken

知道这一点,我们可以开始添加形状:

export let model = new Zdog.Illustration({
  element: ".zdog-canvas",
  resize: true,
  rotate: { x: rotation.initialX, y: rotation.initialY },
  onResize: function (width, height) {
    this.zoom = Math.floor(Math.min(width, height) / sceneSize);
  },
});

let bodyGroup = new Zdog.Group({
  addTo:  model
});

// head
new Zdog.Hemisphere({
  stroke: 6,
  diameter: 15,
  addTo: bodyGroup,
  color: white,
  rotate: { x: Zdog.TAU / 4 },
});

// neck
new Zdog.Cylinder({
  stroke: 6,
  diameter: 15,
  length: 14,
  addTo: bodyGroup,
  color: white,
  frontFace: white,
  backface: white,
  rotate: { x: Zdog.TAU / 4, z: Zdog.TAU },
  translate: { y: 8 },
});

// beak
new Zdog.Cone({
  addTo: model,
  diameter: 4,
  length: 5.5,
  stroke: 2,
  color: yellow,
  backface: yellow,
  rotate: { y: -Zdog.TAU / 4 },
  translate: { x: 11, y: 4 },
});

// beard thing
new Zdog.Shape({
  addTo: model,
  stroke: 3.2,
  path: [
    { x: 11, y: 9 },
    { x: 11, y: 11 },
  ],
  color: red,
});

您可以看到,创建形状很容易。除了尺寸和颜色外,我们还可以设置翻译和旋转。我们可以将形状直接添加到主model对象,或将它们添加到形状中,然后将其添加到model

接下来,我们可以添加眼睛:

// eyes group
let eyesGroup = new Zdog.Group({
  addTo: model,
});

// left eye
let eye1 = new Zdog.Shape({
  addTo: eyesGroup,
  stroke: 1.5,
  color: darkGrey,
  translate: { x: 8, y: -1, z: 3 },
});

// right eye
eye1.copy({
  translate: { x: 8, y: -1, z: -3 },
});

最后的波峰:

// path for each shape
const crestData = [
  {
    path: [
      { x: -4.5, y: -11 },
      { x: -5, y: -11.5 },
    ],
  },
  ...
];


let crestGroup = new Zdog.Group({
  addTo: model,
});

// create all crest shapes
crestData.forEach((item) => {
  new Zdog.Shape({
    addTo: crestGroup,
    stroke: 4.5,
    color: red,
    ...item,
  });
});

此时,我们应该有一个体面的静态鸡肉!

动画事物

接下来,我们可以添加动画。在这个场景中,有两个动画:闪烁和跳跃。

根据文档,可以通过添加“渲染循环”函数来完成更改对场景的更改。此函数通过requestAnimationFrame
在每个帧上运行

function animate() {
  // rotate model each frame
  model.rotate.y += 0.03;
  model.updateRenderGraph();
  // animate next frame
  requestAnimationFrame( animate );
}
// start animation
animate();

我们可以使用相同的原理进行眨眼。

但是,首先,我们如何视觉闪烁?一种方法是使眼睛不可见,又可以迅速看到。但是,一个更好的方法是将白色的形状在眼睛上方,我们向下移动(遮住眼睛),然后向后移动(露出眼睛)。这样,我们得到了眼睑运动,并且由于形状与鸡头的颜色相同,因此我们只有在遮住眼睛时才能看到它:

// eyelid
export let eyelid = new Zdog.Shape({
  addTo: eyesGroup,
  path: [
    { x: 8, y: -3, z: -3.4 },
    { x: 8, y: -3, z: 3.4 },
  ],
  stroke: 2,
  color: white,
});

为了使形状动画,并与ZDOG保持一致,我决定使用class ZAnimation

export class ZAnimation {
  duration: number;
  frame = 0;
  force: number;
  running = false;
  onEnd?: () => void;
  apply: (val: number) => void;

  constructor(options: {
    duration: number;
    force: number;
    interval?: number;
    apply: (val: number) => void;
    onEnd?: () => void;
  }) {
    this.duration = options.duration;
    this.force = options.force;
    this.apply = options.apply;
    this.onEnd = options.onEnd;

    if (options.interval) {
      this.schedule(options.interval);
    }
  }

  schedule(interval: number) {
    setInterval(() => {
      this.running = true;
    }, interval);
  }

  start() {
    this.running = true;
  }

  stop() {
    this.running = false;
  }

  handleFrame() {
    if (!this.running) return;

    this.frame++;

    this.apply(this.force);

    if (this.frame === this.duration) {
      this.frame = 0;
      this.stop();
      this.onEnd?.();
    }
  }
}

让我们看一下类的属性:

  • duration:它应该持续多少帧;
  • frame:这跟踪动画的进度;
  • force:动画形状应在每个帧中变化的数量;
  • apply:将发生的实际转换(例如model.rotate.y += x);
  • running:动画是否正在运行。当frame等于duration时,这将设置为false,因为那是动画的结尾;
  • interval:如果我们希望动画定期发生(眨眼时有用);
  • onEnd:动画结束时运行的可选回调。链接动画时有用。

我们可以这样使用:

const blinkUp = new ZAnimation({
  duration: 18,
  force: 0.1,
  apply: (val) => {
    eyelid.translate.y -= val;
  }
});

const blinkDown = new ZAnimation({
  duration: 18,
  force: 0.1,
  interval: 4000,
  apply: (val) => {
    eyelid.translate.y += val;
  },
  onEnd: () => {
    blinkUp.start();
  }
});

function animate() {
  model.updateRenderGraph();

  blinkDown.handleFrame();
  blinkUp.handleFrame();

  requestAnimationFrame(animate);
}

animate();

我们创建动画,并指定blinkDown应该每4秒发生一次,并且结束时应调用blinkUp。这就是它的样子:

blinking chicken

跳跃呢?为此,我们可以添加更多链式动画,并添加一个事件侦听器,以便我们的鸡肉可以单击:

const bounceDown = new ZAnimation({
  duration: 28,
  force: 1,
  apply: (val) => {
    model.translate.y += val;
  }
});

const bounceUp = new ZAnimation({
  duration: 28,
  force: 1,
  onEnd: () => bounceDown.start(),
  apply: (val) => {
    model.translate.y -= val;
  }
});

document.addEventListener("click", () => {
  bounceUp.start();
});

voilé,它跳跃。

jumping chicken

轻松

很好!现在,我们有眨眼和跳鸡。很好,但是您可能已经注意到跳跃有点平淡。它们看起来太机械,因为运动是线性的。为了使其看起来很自然,我们需要添加一些轻松的东西。我建议您查看easings.net,这是我在这里使用的此主题的绝佳资源。除了显示不同宽松功能的外观外,它还具有CSS和数学功能的实现。我实际上在此演示中使用了这些精确的功能。

基本上,我们希望在每个帧中施加的力都会改变,这取决于当前帧相对于我们指定的总持续时间是什么:

linear movement vs eased

在第一个图中,更改是线性的,这意味着它随着时间的推移(如我们现在的鸡肉)而以一致的方式发生。在右边,我们有一个“轻松”的动作,MeshPhongMaterial最初会缓慢增加,然后在动画的中间更快地增加,然后在末尾放慢脚步。

我们可以在我们的ZAnimation类中添加宽松:

class ZAnimation {
    // ...
    easing: Easing = "easeOutCirc";
    easeAcc = 0;

    constructor(options: {
        // ...
        easing: Easing;        
    }) {
        // ...
        this.easing = options.easing;        
    }

    // ...

    handleFrame() {
        if(!this.running) return;

        this.frame++;

        const value = this.calcEasedValue();
        this.apply(value);

        if (this.frame === this.duration) {
          this.frame = 0;
          this.easeAcc = 0;
          this.stop();
          this.onEnd?.();
        }
    }

    calcEasedValue() {
        const progressDecimal = (this.frame * 100) / this.duration / 100;

        let easedVal = 0;

        switch (this.easing) {
          case "easeOutCirc":
            easedVal = easeOutCirc(progressDecimal);
            break;

          case "easeOutBounce":
            easedVal = easeOutBounce(progressDecimal);
            break;
        }

        const frameEase = easedVal - this.easeAcc;
        this.easeAcc += frameEase;

        return frameEase * this.force;
    }
}

让我们解决这个问题:当我们创建动画时,我们可以指定所需的宽松函数(在这种情况下,只有两个选项easeOutCirceaseOutBounce)。

然后,在handleFrame函数中,我们不仅仅是使用force属性,而是为每个特定帧计算宽松的力。为此,我们需要确定当前的进度,从0到1:

 const progressDecimal = (this.frame * 100) / this.duration / 100;

并将该值传递给easeOutCirceaseOutBounce函数,然后依次返回0到1之间的值。这些函数在哪里?好吧,我从easings.net复制了它们!他们看起来像这样:

function easeInOutCubic(x: number): number {
    return x < 0.5 ? 4 * x * x * x : 1 - Math.pow(-2 * x + 2, 3) / 2;
}

请注意,我们从宽松功能中获得的值是累积的,这意味着它的输出将从0继续增加,直到上次我们称之为1,这不是我们想要的。我们希望每个帧的增量更改。为了做到这一点

const frameEase = easedVal - this.easeAcc;
this.easeAcc += frameEase;

return frameEase * this.force;

最后,我们将框架的宽松值乘以force。这就是我们的形状在每个帧中移动的数量。

要使用此更新的类,我们只需要在实例中添加宽松类型:

const bounceDown = new ZAnimation({
  duration: 32,
  force: 6.8,
  easing: "easeOutBounce",
  addTo: scene,
  apply: (val) => {
    model.translate.y += val;
  },
});

const bounceUp = new ZAnimation({
  duration: 28,
  force: 6.8,
  easing: "easeOutCirc",
  addTo: scene,
  onEnd: () => bounceDown.start(),
  apply: (val) => {
    model.translate.y -= val;
  },
});

跳跃看起来 lot 现在更好。

eased jump

包起来

您可能已经注意到,在演示中,鸡也跟随鼠标。您可以查看为此所需的代码,尽管我不会在这篇文章中深入研究。谁知道,我们会回来第二部分。

如果您想知道什么时候需要伪3D鸡跳和眨眼,我将无法回答。

但是,如果您需要的是一些视觉才能,并且不需要更高级的功能,那么ZDOG可能是一个绝佳的选择。它非常易于使用,并提供了一组默认形状和工具,可帮助您快速启动并运行。结合一些自定义动画和用户互动,您可以创建一个令人愉快的结果。

一如既往,感谢您的阅读!