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

我正在使用Eloquent JavaScript中遇到的一个分形图递归函数。

我想为每个分支的绘制设置延迟-以便在我修补此函数及其参数时可视化分支/递归调用的流。

我在下面的代码中使用setTimeout的方法似乎不起作用,我不知道原因。

我希望cx.fillRect(...)为每个延迟绘制一个分支;而不是堆叠在队列中,因为setTimeout之外没有其他代码可等待。

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

我使用setTimeout的非工作尝试:

数据-lang="js"数据-隐藏="假"数据-控制台="真"数据-巴贝尔="假">
<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>

无延迟工作代码:

数据-lang="js"数据-隐藏="假"数据-控制台="真"数据-巴贝尔="假">
<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会延迟第一次调用,然后为其子树生成两个具有相同超时的递归调用,导致它们相互遍历,以此类推。

所有递归调用都需要等待整个左子树完成绘制,然后才能移到右子树,这也需要在调用可以解析之前完成,并让父级继续下一个操作(右子树或解析)。不能让两个不同的调用帧同时处理同一画布堆栈。

我会使用Promise来实现这一点;这允许您管理setTimeout的顺序,并使用sleep函数设置所需的延迟,基本上是简化的setTimeout

数据-lang="js"数据-隐藏="假"数据-控制台="真"数据-巴贝尔="假">
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>

为了进行比较,下面介绍了如何在没有承诺的情况下使用回调来完成此操作,每个子级都会触发该回调,以通知其父级已完成操作,这样父级就知道何时绘制下一个子树或解析:

数据-lang="js"数据-隐藏="真"数据-控制台="真"数据-巴贝尔="假">
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>

因为您是在画布上制作动画,所以可以考虑使用requestAnimationFrame循环访问绘制每个帧的生成器函数。RAF提供better-quality animations than setTimeout

数据-lang="js"数据-隐藏="真"数据-控制台="真"数据-巴贝尔="假">
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>

相关文章