如何在分形绘图递归函数中创建延迟

How to create delay in fractal drawing recursive function

我正在玩 Eloquent JavaScript 中遇到的分形绘图递归函数。

我想为每个分支的绘制设置延迟 - 目的是在修改此函数及其参数时可视化 branches/recursive 调用的流程。

我在下面的代码中使用 setTimeout 的方式似乎不起作用,我很困惑为什么。

我预计 cx.fillRect(...) 每次延迟都会画一个分支;而不是堆叠在队列中,因为 setTimeout 之外没有其他代码等待。

下面我首先包含了工作分形绘图 html/js 代码,没有尝试包含延迟。第二段代码是我尝试包含 setTimeout 延迟。

我尝试使用 setTimeout 的无效尝试:

<canvas width="600" height="300"></canvas>
<script>
  let cx = document.querySelector("canvas").getContext("2d");

  function branch(length, angle, scale) {
    setTimeout(() => {
      cx.fillRect(0, 0, 1, length);
      if (length < 8) return;
      cx.save();
      cx.translate(0, length);
      cx.rotate(-angle);
      branch(length * scale, angle, scale);
      cx.rotate(2 * angle);
      branch(length * scale, angle, scale);
      cx.restore();
    }, 80);
  }
  cx.translate(300, 0);
  branch(60, 0.5, 0.8);
</script>

没有延迟的工作代码:

<canvas width="600" height="300"></canvas>
<script>
  let cx = document.querySelector("canvas").getContext("2d");

  function branch(length, angle, scale) {
    cx.fillRect(0, 0, 1, length);
    if (length < 8) return;
    cx.save();
    cx.translate(0, length);
    cx.rotate(-angle);
    branch(length * scale, angle, scale);
    cx.rotate(2 * angle);
    branch(length * scale, angle, scale);
    cx.restore();
  }
  cx.translate(300, 0);
  branch(60, 0.5, 0.8);
</script>

setTimeout 的尝试延迟了第一次调用,然后对其具有相同超时的子树产生两次递归调用,导致它们相互走过,依此类推。

所有递归调用都需要等待整个左子树完成绘制,然后再继续右子树,右子树也需要完成,然后调用才能解析并让父级继续进行下一个操作(右子树或解决)。您不能同时让两个不同的调用帧弄乱同一个 canvas 堆栈。

我会使用 promises 来做到这一点;这使您可以管理 setTimeout 的顺序并使用 sleep 函数设置所需的延迟,基本上是一个 promisified setTimeout.

const sleep = ms => new Promise(
  resolve => setTimeout(resolve, ms)
);

const cx = document.querySelector("canvas").getContext("2d");

async function branch(length, angle, scale) {
  cx.fillRect(0, 0, 1, length);

  if (length < 8) {
    return;
  }

  await sleep(50); // delay in ms, good to make into a parameter
  cx.save();
  cx.translate(0, length);
  cx.rotate(-angle);
  await branch(length * scale, angle, scale);
  cx.rotate(2 * angle);
  await branch(length * scale, angle, scale);
  cx.restore();
}

cx.translate(300, 0);
branch(60, 0.5, 0.8);
<canvas width="600" height="300"></canvas>

为了比较,下面是如何在没有 promise 的情况下使用回调来做到这一点,每个子级都会触发回调以向其父级发出信号,告知其已完成,以便父级知道何时绘制下一个子树或解析:

const cx = document.querySelector("canvas").getContext("2d");

function branch(length, angle, scale, done) {
  cx.fillRect(0, 0, 1, length);

  if (length < 8) {
    done && done();
    return;
  }

  setTimeout(() => {
    cx.save();
    cx.translate(0, length);
    cx.rotate(-angle);
    branch(length * scale, angle, scale, () => {
      cx.rotate(2 * angle);
      branch(length * scale, angle, scale, () => {
        cx.restore();
        done && done();
      });
    });    
  }, 50);
}

cx.translate(300, 0);
branch(60, 0.5, 0.8);
<canvas width="600" height="300"></canvas>

由于您要在 canvas 上制作动画,您可以考虑使用 requestAnimationFrame 循环绘制每一帧的生成器函数。英国皇家空军提供 .

const cx = document.querySelector("canvas").getContext("2d");

function *branch(length, angle, scale) {
  cx.fillRect(0, 0, 1, length);

  if (length < 8) {
    return;
  }

  yield;
  cx.save();
  cx.translate(0, length);
  cx.rotate(-angle);
  yield *branch(length * scale, angle, scale);
  cx.rotate(2 * angle);
  yield *branch(length * scale, angle, scale);
  cx.restore();
}

cx.translate(300, 0);
const branchGen = branch(60, 0.5, 0.8);
const speedMs = 50;
let lastTime = 0;
let done = false;

(function drawFrame(time) {
  !done && requestAnimationFrame(drawFrame);

  if (time && lastTime === 0) {
    lastTime = time;
  }
  else if (lastTime > 0 && time >= lastTime + speedMs) {
    ({done} = branchGen.next());
    lastTime += speedMs;
  }
})();
<canvas width="600" height="300"></canvas>