1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

requestAnimationFrameで再帰っぽいロジックを使わずLoop構造を維持したままアニメーションを実現する(かつ速度調整も可能にする)

Last updated at Posted at 2025-05-24

サンプル

円を描く

アニメーションを考慮せず、canvasに円を描くことを考えます
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');
const centerX = 150;
const centerY = 150;
const radius = 100;
const numPoints = 360; // ドットの数

function drawCircle() {
  // 描画する関数
  function draw(x, y) {
    ctx.beginPath();
    ctx.arc(x, y, 1, 0, 2 * Math.PI);
    ctx.fillStyle = 'red';
    ctx.fill();
    ctx.closePath();
  }

  for (let i = 0; i < numPoints; i++) {
    const angle = 2 * Math.PI * i / numPoints;
    const x = centerX + radius * Math.cos(angle);
    const y = centerY + radius * Math.sin(angle);

    draw(x, y);
  }
}

drawCircle();

円をアニメーションで表示

ネットで検索したりAIに作成させたりすると、再帰っぽい感じでrequestAnimationFrame()関数で自分自身を呼び出すコードになります
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');
const centerX = 150;
const centerY = 150;
const radius = 100;
const numPoints = 360; // ドットの数
let currentPoint = 0;

function drawCircle() {
   // 描画する関数
  function draw(x, y) {
    ctx.beginPath();
    ctx.arc(x, y, 1, 0, 2 * Math.PI);
    ctx.fillStyle = 'red';
    ctx.fill();
    ctx.closePath();
  }

  if (currentPoint < numPoints) {
    const angle = 2 * Math.PI * currentPoint / numPoints;
    const x = centerX + radius * Math.cos(angle);
    const y = centerY + radius * Math.sin(angle);

    draw(x, y);
    currentPoint++;

    requestAnimationFrame(drawCircle);
  }
}

drawCircle();

ここでプロミスを使うと元のLoop構造を残した形に出来ます
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');
const centerX = 150;
const centerY = 150;
const radius = 100;
const numPoints = 360; // ドットの数

async function drawCircle() {
  // 描画する関数
  function draw(x, y) {
    ctx.beginPath();
    ctx.arc(x, y, 1, 0, 2 * Math.PI);
    ctx.fillStyle = 'red';
    ctx.fill();
    ctx.closePath();
  }

  for (let i = 0; i < numPoints; i++) {
    const angle = 2 * Math.PI * i / numPoints;
    const x = centerX + radius * Math.cos(angle);
    const y = centerY + radius * Math.sin(angle);

    await new Promise(resolve => {
      requestAnimationFrame(() => {
        draw(x, y);
        resolve();
      });
    });
  }
}

drawCircle();

アニメーション速度を調整可能にする

さらにもっと発展させて描画Speedをコントロールできるようにします

そのためのAnimationDrawerクラスを考えました
使い方は、コード内のコメントおよびサンプルコードを参照ください

//////////////////////////////////////////////////////////////////////////
/**
 * アニメーション付きで描画処理を行うためのクラス。
 * 1フレームあたりの描画回数を指定してアニメーションのSpeedを調整可能
 * @class AnimationDrawer
 * @example
 * const drawer = new AnimationDrawer(drawFunc, 2);
 * drawer.draw(x, y);
 * drawer.flush();
 */
//////////////////////////////////////////////////////////////////////////
class AnimationDrawer {
  // 1フレームあたりの描画回数(0の場合はアニメーションなし)
  #drawPerFrame;
  // 待機中の描画関数の引数をキューイングする配列
  #pendingArgs = [];
  // 描画関数
  #drawFunc;

  /**
   * コンストラクタ
   * @param {function} drawFunc 描画処理を行う関数
   * @param {number} drawPerFrame 1フレームあたりの描画回数
   *       (デフォルトは1、0の場合はアニメーションなし)
   */
  constructor(drawFunc, drawPerFrame = 1) {
    this.#drawFunc = drawFunc;
    this.#drawPerFrame = drawPerFrame;
  }

  /**
   * 描画命令をキューに追加
   * @param {...any} args - 描画関数の引数
   */
  draw(...args) {
    // drawPerFrameが0の場合はアニメーションなし
    if (this.#drawPerFrame === 0) {
      this.#drawFunc(...args);
      return Promise.resolve();
    }
    this.#pendingArgs.push(args);
    if (this.#pendingArgs.length >= this.#drawPerFrame) {
      return this.flush();
    }
    return Promise.resolve();
  }

  /**
   * キューにある描画命令を実行
   * @returns {Promise<void>}
   */
  flush() {
    if (this.#pendingArgs.length === 0) return Promise.resolve();
    return new Promise(resolve => {
      requestAnimationFrame(() => {
        for (const arg of this.#pendingArgs) {
          this.#drawFunc(...arg);
        }
        this.#pendingArgs.length = 0;
        resolve();
      });
    });
  }
}
AnimationDrawerクラスを使ったサンプルコード
const DRAW_PER_FRAME = 2; // 1フレームあたりの描画数(速度調整用)
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');
const centerX = 150;
const centerY = 150;
const radius = 100;
const numPoints = 360; // ドットの数

async function drawCircle() {
  // 描画する関数
  function draw(x, y) {
    ctx.beginPath();
    ctx.arc(x, y, 1, 0, 2 * Math.PI);
    ctx.fillStyle = 'red';
    ctx.fill();
    ctx.closePath();
  }

  const animationDrawer = new AnimationDrawer(draw, DRAW_PER_FRAME);
  for (let i = 0; i < numPoints; i++) {
    const angle = 2 * Math.PI * i / numPoints;
    const x = centerX + radius * Math.cos(angle);
    const y = centerY + radius * Math.sin(angle);

    await animationDrawer.draw(x, y);
  }
  // DRAW_PER_FRAME引数を省略した(デフォルトの1)場合は不要
  await animationDrawer.flush();
}

drawCircle();
アニメーションする描画関数の上位関数にはすべて awaitをつける必要があります

ここでお試しが出来ます

See the Pen アニメーションで円を描くサンプル by takanaweb5 (@takanaweb5) on CodePen.

実例

以下の記事のお絵かきロジックのパズルを解く様子のアニメーションにこの手法を使用しています

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?