【Dart】DartでWebコンテンツを作ってみる その3 〜Timerを使用したアニメーション〜

この記事は約10分で読めます。

本記事について

この記事では、TimerとCanvasElementを使用したアニメーションの実装方法について記載しています。まず、Timerクラスの使い方について説明し、その後にCanvasElementと組み合わせたアニメーションの実装について説明します。

Timerの使い方

dart:asyncにはTimerクラスが用意されています。Timerクラスを使用すると主に下記2つのことを行うことができます。

  • 指定時間後に関数を1度実行する
  • 指定間隔で繰り返し関数を実行する

指定時間後に1度実行

アニメーションには向きませんが、まずは前者の使い方を見てみましょう。下にサンプルコードを示します。

import 'dart:html';

// timerを使用するのに必要
import 'dart:async';

// numをもとにtextを更新する関数
void updateText(Text text, int num) {
  text.data = '$num';
}

void main(List<String> arguments) {
  // bodyの準備
  final body = document.body;
  if (body == null) {
    return;
  }

  // 数値とテキストの準備
  var num = 0;
  final text = Text('');
  updateText(text, num);

  // ボタンとテキストの配置
  body.append(ButtonElement()
    ..text = 'Increment'
    ..onClick.listen((event) {
        // 1秒後にnumをインクリメントしてupdateTextを実行
      Timer(Duration(seconds: 1), () {
        updateText(text, ++num);
      });
    }));
  body.append(text);
}

このコードを実行すると、下のようにボタンを押してから1秒後に数値がインクリメントされます。

Timerコンストラクタは以下のように2つの引数を取ります。

引数内容
durationDuration時間を指定
callbackvoid Function()指定時間後に実行される関数

Durationはミリ秒やナノ秒単位での時間指定も出来ますが、今回の例では1秒単位で1、つまり1秒の時間指定をしています(28行目)。callbackに指定した関数の中では、数値を管理しているnumをインクリメントして文字列を表示しているtextを更新しています(29行目)。

ここまででTimerを使用して指定時間後に1度だけ実行する方法の説明は終わりです。

指定間隔で定期的に実行

アニメーションの場合は定期的に画面の更新を行いたいので、Timer.periodicコンストラクタを使用して定期的に関数が呼ばれるようにする必要があります。下にサンプルコードを示します。

import 'dart:html';

// timerを使用するのに必要
import 'dart:async';

// numをもとにtextを更新する関数
void updateText(Text text, int num) {
  text.data = '$num';
}

void main(List<String> arguments) {
  // bodyの準備
  final body = document.body;
  if (body == null) {
    return;
  }

  // 数値とテキストの準備
  var num = 0;
  final text = Text('');
  updateText(text, num);

  // 定期的に実行するタイマーの準備
  Timer.periodic(Duration(seconds: 1), (timer) {
    updateText(text, ++num);
  });

  // ボタンとテキストの配置
  body.append(text);
}

このコードを実行すると、下のように1秒ごとに数値がインクリメントされていきます。

Timer.periodicコンストラクタは以下のように2つの引数を取ります。

引数内容
durationDuration時間を指定
callbackvoid Function(Timer)指定時間間隔で定期的に実行される関数

durationはTimerコンストラクタと同様に時間を指定します。今回の例でも1秒を指定しています(24行目)。一方、callbackの型は変化しています。Timerコンストラクタでは引数を取らない関数でしたが、Timer.periodicコンストラクタではTimer型の引数を取る関数になっています。この引数の用途は次節で説明します。

定期的な実行の停止

定期的な関数の実行をある期間だけ行いたい場合もあると思います。そこで、Timer.periodicによる定期的な実行を止める方法について見ていきます。と言っても簡単で、前節で説明したようにTimer.periodicにより定期的に実行される関数がTimer型の引数を取るので、cancel関数を呼んであげるだけです。コード例としては以下のようになります。

import 'dart:html';

// timerを使用するのに必要
import 'dart:async';

// numをもとにtextを更新する関数
void updateText(Text text, int num) {
  text.data = '$num';
}

void main(List<String> arguments) {
  // bodyの準備
  final body = document.body;
  if (body == null) {
    return;
  }

  // 数値とテキストの準備
  var num = 0;
  final text = Text('');
  updateText(text, num);

  // タイマーの宣言
  Timer? timer;

  // ボタンの準備
  const kStart = 'start', kStop = 'Stop';
  final button = ButtonElement()..text = kStart;

  // ボタンを押したときの挙動を記述
  button.onClick.listen((event) {
    if (button.text == kStart) {
      button.text = kStop;
      timer = Timer.periodic(Duration(seconds: 1), (timer) {
        updateText(text, ++num);
      });
    } else {
      button.text = kStart;
      timer?.cancel();
    }
  });

  // ボタンとテキストの配置
  body.append(button);
  body.append(text);
}

実行すると下記のようにボタンを押す度に定期的な実行状態と停止状態が切り替わります。

ボタンの表示がStopになっているときにはcancel関数を呼ぶことでタイマーを停止しています(39行目)。これにより、数値の更新を止めることができます。

ここまでで、Timerの大まかな使い方の説明は以上になります

アニメーション

ここまで長々とTimerの説明をしてきましたが、ここからはCanvasRenderingContext2Dと組み合わせてアニメーションを実装してみます。と言ってもTimerから定期的に呼ばれる関数でCanvasRenderingContext2Dを使った描画を実行するだけです。今回は円が残像を残しながら動くアニメーションを実装してみました。コードは下記のようになります。

import 'dart:html';
import 'dart:async';
import 'dart:math';

void draw(CanvasRenderingContext2D context, double t, [bool first = false]) {
  // 描画領域のサイズを取得
  final width = context.canvas.width, height = context.canvas.height;
  if (width == null || height == null) {
    return;
  }

  // 描画領域のサイズをもとに円の半径を決定
  final r = min(width, height) * 0.1;

  // tの値をもとに円の位置を計算
  final x = r + (sin(t) + 1.0) * 0.5 * (width - 2.0 * r),
      y = r + (sin(2.0 * t) + 1.0) * 0.5 * (height - 2.0 * r);

  // 背景の塗りつぶし(初回以外は透明度を設定することにより円の残像を残す)
  context.setFillColorRgb(224, 192, 192, first ? 1.0 : 0.3);
  context.fillRect(0, 0, width, height);

  // 円の描画
  context.beginPath();
  context.ellipse(x, y, r, r, 0.0, 0.0, 2.0 * pi, null);
  context.closePath();
  context.setFillColorRgb(64, 64, 64);
  context.fill();
}

void main(List<String> arguments) {
  // bodyの準備
  final body = document.body;
  if (body == null) {
    return;
  }

  // タイマーとタイマーとともに進む時刻tの準備
  var t = 0.0;
  Timer? timer;

  // CanvasElementの準備
  final canvas = CanvasElement();
  draw(canvas.context2D, t, true);

  // ButtonElement関係の準備
  const kStart = 'Start', kStop = 'Stop';
  final button = ButtonElement()..text = kStart;

  // ボタンを押したときの挙動の実装
  button.onClick.listen((event) {
    if (button.text == kStart) {
      timer = Timer.periodic(Duration(milliseconds: 1000 ~/ 60), (timer) {
        draw(canvas.context2D, t += 0.02);
        button.text = kStop;

        if (t > 2.0 * pi) {
          t -= 2.0 * pi;
        }
      });
    } else {
      timer?.cancel();
      button.text = kStart;
    }
  });

  // CanvasElementとButtonElementの配置
  body.append(canvas);
  body.append(Element.br());
  body.append(button);
}

これを動作させると下記のようになります。

今回はアニメーションのため、Timerによる関数の実行がおおよそ1秒間に60回行われるように指定しています(53行目)。描画を行っているのはdraw関数ですが、アニメーションは描画内容が変化していく必要があるためdouble型の引数tを取るようにしています。そして、引数tに応じて計算した座標に基づいて円の描画を行うようにしています。また、引数tはタイマーによる関数の実行が行われる度に数値が増加するようになっています(54行目)。

また、draw関数の中では前回の描画内容を背景の塗りつぶしにより消去していますが、透明度を設定することにより完全に消去はせず、過去に描画した円が少し残るようにしています(20行目)。ただし、背景の塗りつぶしが繰り返し行われるため、円はずっと残るわけではなく徐々に背景と馴染んで消えていきます。これにより残像の表現を実現しています。

まとめ

本記事ではTimerクラスの使い方とTimerクラスを使用したアニメーションの実装方法について説明しました。次回は、今回含めることのできなかった3D描画(WebGL)について書いていきたいと思います。

コメント

タイトルとURLをコピーしました