フロントエンドの勉強がてら、ちょっとしたタイマーアプリを作ってみた。

機能としてはごくごく普通のタイマーだ。iPhoneのタイマーのように時間を決めて開始すればよい。 途中で一時停止できるし、リセットもできる。 ただそこにプラスして、時間が経つにつれて背景の波がどんどん引いていくような演出を施した。

ここでは、このアプリを作る上でポイントとなるところをいくつかピックアップして備忘録的に書き記していく。

タイマーの状態遷移

タイマーは大まかに、以下の4つの状態を行き来していると考えられる。

  1. ユーザの時間設定を受け付ける状態(Ready)
  2. タイマーを実行する状態(Running)
  3. タイマーを一時停止した状態(Stopping)
  4. タイムアップし、リセット処理を行う状態(Reset)

図に表すとこのような感じだろう。

黒線はユーザまたはアプリ側の操作によって遷移することを表す。 青線はユーザ(アプリ)の操作を待機している状態、赤線は強制的に遷移することを表している。 Reset以外の3状態はボタン操作をされない限りはずっとそのままとどまっており、 Resetは有無をいわさずReadyへ移る。

これら4つの状態はフラグ(上図ongoing, stop)を使うことで表現できる。 フラグは立つ(true)か下りる(false)かの2通りを表す変数で、2つ使うと2×2=4通りの組み合わせが出る。 これらを下表のように各状態に当てはめればプログラム上で状態遷移を実装可能だ。

タイマーの実装

タイマーの処理部分はVue.jsで書いた。採用理由は単純にイケてそうだから。 以下は特に状態遷移に関わる部分だけを抜粋している。

上図2.タイマー実行中の状態はrunTimerが実現している。 async, awaitを使って非同期で0.1秒待ってから残り時間を減らすという処理を繰り返している。

  data: {
    set_ms: 0,
    remain: 0,
    ongoing: false,
    stop: false
  },
  methods: {
    startTimer: function() {
        this.set_ms = this.setTime();
        this.remain = this.set_ms;
        this.stop = false;
        this.ongoing = true;
        this.runTimer();
    },
    runTimer: async function() {
        while (this.ongoing) {
            await sleep(100);
            this.remain -= 100;
            if (this.remain <= 0) {
                this.finishTimer();
                break;
            }
        }
    },
    finishTimer: function() {
      // beep!
        window.alert("Time's up!");
        this.stop = true;
        this.resetTimer();
    },
    stopTimer: function() {
        this.ongoing = false;
        this.stop = true;
    },
    resumeTimer: function() {
        this.ongoing = true;
        this.stop = false;
        this.runTimer();
    },
    resetTimer: function() {
        this.ongoing = false;
        this.stop = false;
        this.set_ms = this.setTime();
        this.remain = this.set_ms;
    }
  }
});

水のアニメーションの実装

砂が落ちていくようなアニメーションだと少しインパクトに欠けると感じ、 思い切ってお風呂のように水を流しきるようなアニメーションにしてみようと思った。 それでもまだ波風立たずに水量が下がっていくだけだと退屈なので、波を立たせるような演出もプラスした。

この演出は以下の動画を参考にして作った。 Adobe XDでアニメのプロトタイプを作成し、図をsvgとして書き出してhtmlに埋め込んだ。

波を動かすアニメーションはCSSでは下記のように実装した。

波は前面と背面の2つに分けて動かしている。 前面と背面で動作する方向は逆にしてエンドレスにスライドさせるようにアニメーションを施した。 もともとは前面背面ともに2つの同じ形の波がつながっているのだが、画像幅を2倍にして画面上の波を1つにしている。 スライド幅を幅の半分の長さに指定するとアニメーションを終えても同じ形・位置から再開されるため、ずっと波が動いているように見える。

.waterwave {
  position: absolute;
  top: 0;
  width: 200%;
}

@keyframes wavegrad {
  from {
    left: -100%;
  }

  to {
    left: 0%;
  }
}

@keyframes wavegrad-back {
  from {
    left: 0%;
  }

  to {
    left: -100%;
  }
}

.back {
  animation-duration: 20s;
  animation-fill-mode: none;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
  animation-name: wavegrad-back;
  fill: #2e55be;
}

.front {
  animation-duration: 20s;
  animation-fill-mode: none;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
  animation-name: wavegrad;
  fill: #8ba5ea;
}

タイマー動作中に波の高さが下がっていくアニメーションは、Running状態に波のコンポーネントに runningクラスを付加させ、topプロパティを減算して演出している。 これはVueの算出プロパティを使って実装した。

あらかじめ波にtransitionプロパティを設定すると、 動作中だけでなくリセット時のアニメーションが自然になる。

computed: {
    running: function() {
        return {
            active: this.ongoing || this.stop,
            top: this.wave_height + "%"
        }
    }
}

まとめ

タイマーを実装するにしても一筋縄には行かないなと思った。実際に手を動かして作ってみるの本当に大事だ。 アニメーションはCSSだけでここまで表現できるのかと単純に感動した。 PCでアニメーションを表現するのは小学生の時にFlash Makerで遊んで以来だ。

あのときの思い出が蘇り、モノを動かす意欲が湧いた気がする。

ちょっとしたアイデアでもノートにしたためてどんどん実現できるようになるといいな。