miaoyu

用Promises实现动画

miaoyu · 2017-06-13翻译 · 541阅读 原文链接

最近做了一个花瓣从树上飘落的动画,树是通过随机算法生成的(主要是受到这个Demo的启发),然后在树枝的末端随机生成小花。

这个动画我用了Promises来帮我实现。这将涉及到异步和递归。

一棵树慢慢长大

从小小的种子。。。

第一步,使用递归随机生成一颗树,以下是一些关键的点:

  1. 从画树干开始——树干的的起点(地面),树干的终点(树干离地面的高度) .

  2. 画树干。

  3. 接下来画一些树枝——它们从树干的末端开始画,我们分别选择左右两个随机角度来延伸树枝,树枝的长度是比树干稍短的随机长度,这样就可以计算出新树枝的长度。

  4. 画树枝。

  5. 对于这两条新的树枝,我们获取到它们的末端位置,然后再长出两条新的树枝,就像第3步那样。

  6. 重复第5步,直到生成一颗茂密的大树!

递归算法可以生成一棵树,但是我们如何模拟一棵树缓慢生长的动画呢?ok,我们可以加延迟!

在第3步到第4步之间,画新树枝的时候,我们可以延迟(使用setTimeout)。由于重复在画新的分支,树将缓慢的呈扇形生长,计算,等待,生成,计算,等待,生成。。。

所以我来描述下我们即将要完成的代码(未定义的变量只是用于描述):

// 树枝按照指定的初始点,长度,角度被渲染生成。
function growBranch({ startingPoint, length, angle, remainingBranches }) {
  // 通过computeEndpoint方法来获取到树枝结束点
  endingPoint = computeEndpoint({ startingPoint, length, angle });
  // 渲染树枝
  renderBranch({ startingPoint, endingPoint });
  // 计算出剩下需要渲染的树枝,每次减一,当为0时,结束渲染
  const newRemainingBranches = remainingBranches - 1;
  if (newRemainingBranches <= 0) {
    return;
  }
  // 随机的生成左右两条树枝
  ["left", "right"].forEach(direction => {
    const newAngle = computeRandomAngle(direction);
    const newLength = shrinkBranchLength(length);
  // 递归异步
    setTimeout(function() {
      growBranch({
        startingPoint: endingPoint,
        length: newLength,
        angle: newAngle,
        remainingBranches: newRemainingBranches
      });
    }, DELAY);
  });
}

// 通过调用生成树枝方法来生成树
function growTree() {
  growBranch({
    startingPoint: GROUND,             // 从地面开始生长
    length: TRUNK_LENGTH,              // 初始化长度
    angle: STRAIGHT_UP,                // 第一次生成的是主干,所以是90度 
    remainingBranches: TREE_COMPLEXITY // 最多生成多少级的树枝
  });
}

找到最后生成的树枝

一旦树画好了,我们就知道了最后生成的树枝,下个阶段我们在这些树枝的末端画花。具体来说,当我意识到newRemainingBranches这个变量小于等于0时候,意味着不会产生新的树枝了,因此我们可以知道所有最后生成树枝的末端位置。

因为我们加了延迟,所以我们需要知道什么时候所有末端树枝能渲染完。

如果所有树枝画好后,能通知我们就好了。

Promises

Promises是实现上述要求的最好方法!我们将得到最后生成树枝末端位置的数组,我们在这些位置上画花。

具体来说就是当growTree()调用growBranch()时候,growBranch()将会递归调用自己,在growBranch()方法中,判断newRemainingBranches <= 0时(也就是不再产生新树枝时),将末端位置作为resolve()方法的参数汇集成一个数组。

// 树枝按照指定的初始点,长度,角度被渲染生成。
function growBranch({ startingPoint, length, angle, remainingBranches }) {
  endingPoint = computeEndpoint({ startingPoint, length, angle });
  renderBranch({ startingPoint, endingPoint });

  const newRemainingBranches = remainingBranches - 1;
  if (newRemainingBranches <= 0) {
    // 返回一个包含树枝的末端位置参数的回调
    return Promise.resolve(endingPoint);
  }
  // 使用Promise.all方法来保证,一旦左右两个树枝完成,就返回末端位置。
  return Promise.all(
    ["left", "right"].map(direction => {
      const newAngle = computeRandomAngle(direction);
      const newLength = shrinkBranchLength(length);

      // 包裹setTimeout,递归调用
      return new Promise(resolve => {

        setTimeout(function() {

          resolve(
            growBranch({
              startingPoint: endingPoint,
              length: newLength,
              angle: newAngle,
              remainingBranches: newRemainingBranches
            })
          );
        }, DELAY);
      });
    })
    // 得到最后生成树枝末端位置的数组
  ).then(flatten);
}

// 通过调用生成树枝方法来生成树
function growTree() {
  return growBranch({
    startingPoint: GROUND,
    length: TRUNK_LENGTH,
    angle: STRAIGHT_UP,
    remainingBranches: TREE_COMPLEXITY
  });
}

下面是一些Promise对象的方法:

  • Promise.resolve()是一个很有实用方法,在异步操作成功后调用,并将参数传递出去。

  • Promise.all() 另外一个很实用的方法,它的参数是一个promise对象数组,当数组里所有函数执行完毕,然后打包返回。

  • new Promise() Mozilla简洁深入的解释.

  • Promise.prototype.then() 链式调用结束后调用

最后的成果

下面是生成一棵树的完整代码:

运行以上代码后,可看到console打印出了一个包含所有树枝末端位置的数组。还有你会发现和之前的代码有很大的不同,是因为之前的代码只是用于梳理逻辑的。

最后,我们就可以画花了,以下是整个demo的完整代码:

如果你也用到了类似的技术来解决问题,请在我的twitter @OngEmil 告诉我🌸🌸🌸


相关文章