PLAY DEVELOPERS BLOG

HuluやTVerなどの日本最大級の動画配信を支える株式会社PLAYが運営するテックブログです。

HuluやTVerなどの日本最大級の動画配信を支える株式会社PLAYが運営するテックブログです。

再生プレイヤーの UI 改修でアニメーションとグラデーションの実装に試行錯誤した話

こんにちは、SaaSプロダクト開発部の田澤です。フロントエンドをメインに、再生プレイヤーの開発を行っています。
今回は、当社動画配信プラットフォームの一つであるULIZAの、プレイヤーUI改修にて試行錯誤した話をさせていただこうと思います。

プレイヤーUI改修概要

プレイヤーUIの改修にあたって、具体的には、

  • インタラクションを追加
  • シークバーのグラデーション設定を追加
  • ボタンなどのアイコンを新調
  • メニュー、ツールチップ、リストなどのUIを改修

などを行いました。
インタラクションとは、プレイヤーにフォーカスが置かれている状態で、以下のような操作を行った場合に出るアニメーションを指します。

操作 挙動
センターコントローラーの再生/一時停止操作 再生/一時停止切り替え
スペースキー押下 再生/一時停止切り替え
カーソルキー(↑)押下 音量を5%上げる
カーソルキー(↓)押下 音量を5%下げる
カーソルキー(←)押下 5秒前にシーク
カーソルキー(→)押下 5秒先にシーク
Mキー押下 ミュート切り替え
映像領域をクリック、またはタップ 再生/一時停止切り替え
映像領域左側をダブルタップ (モバイルの場合のみ) 30秒前にシーク
映像領域右側をダブルタップ (モバイルの場合のみ) 30秒先にシーク

どのようなアニメーションが出るかは、以下プレイヤーを操作してご確認ください。

ここでは、インタラクションの実装とシークバーのグラデーション設定について掘り下げたいと思います。
単純なCSS、JavaScriptの操作の話になりますが、お付き合いいただけますと幸いです。

インタラクションの実装

始めに、インタラクションの実装についてです。
以前のプレイヤーでは、トーストで操作内容を表示していたものを、インタラクションに変更します。
そのためには

A. プレイヤー中央部で拡大しながらフェードアウトするインタラクション
B. プレイヤー左右部に表示するインタラクション

の2種類を実装する必要があります。Aは再生/一時停止、または音量調整の場合に使用し、Bはシーク操作の場合に使用します。

CSSの設定

どちらのインタラクションもanimationプロパティを使用します。

Aではアニメーションする時間と速度、アニメーション終了時の状態を指定します。

.interaction-center {
  animation-name: interaction;
  animation-duration: 0.8s;
  animation-timing-function: ease-out;
  animation-fill-mode: forwards;
}

@keyframesで拡大率と透過度を指定します。

@keyframes interaction {
  0% {
    transform: scale(0.7);
    opacity: 1;
  }
  100% {
    transform: scale(1.3);
    opacity: 0;
  }
}

これで、拡大しながらフェードアウトする実装ができました。

Bはインタラクション全体の表示はシンプルですが、アイコンのアニメーションが複雑になります。
Bの場合、インタラクションはフェードアウトなどを行わないため、クラス名のつけ外しで表示/非表示を設定します。

シークのインタラクションでは、再生アイコンを3つ連ね、シーク方向にアイコンを順番にハイライトさせます。順番は「先送り」のシーク操作が行われたときを基準とします。
アイコンのanimationプロパティは以下のように設定します。

.interaction-seek {
  animation-name: interaction-seek;
  animation-duration: 0.4s;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
  animation-direction: alternate-reverse;
  animation-fill-mode: backwards;
}

@keyframesでは、アイコンをハイライトできるように、fillプロパティの色を設定します。

@keyframes interaction-seek {
  0% {
    fill: rgba(255, 255, 255, 1);
  }
  100% {
    fill: rgba(0, 0, 0, 0.1);
  }
}

そして、animation-delayで連なった各アイコンのアニメーション発火タイミングを0.2秒ずつずらすことで、シーク方向にアイコンを順番にハイライトさせます。

.seek-icon:nth-child(1) {
  animation-delay: 0s;
}

.seek-icon:nth-child(2) {
  animation-delay: 0.2s;
}

.seek-icon:nth-child(3) {
  animation-delay: 0.4s;
}

このままだと、「前戻し」のシーク操作を行った場合も先送り方向と同じ順番にハイライトしてしまいます。そのため、前戻しのシーク操作が行われた場合は以下flex-directionプロパティで順番を並べ替えます。

.seek-back {
  flex-direction: row-reverse;
}

これで、シーク方向にアイコンを順番にハイライトさせる実装ができました。

JavaScriptの実装

先程設定したアニメーションは、専用のクラスを追加して実行します。
そして、onanimationendを用いて、アニメーション終了とともにクラス名を削除するようにします。*1

/**
 * インタラクションを表示する関数
 */
function startInteraction() {
  // アニメーション用のクラスを追加
  this.playerEl.classList.add('animating');

  // アニメーションが終了したときの処理
  this.playerEl.onanimationend = () => {
    // アニメーション用のクラスを削除
    this.playerEl.classList.remove('animating');
  }
}

これで、各操作ごとにインタラクションが表示できるようになりました。
しかし、この実装ではインタラクションが終了する前に操作が行われる想定ができていません。ユーザーによっては、キーを連続で押下する、または連続タップを行う可能性があるため、連続操作の対応を行います。
上記の場合、インタラクション表示中に次の操作が行われると、追加したいクラスが既についている状態になります。アニメーション開始の契機を失ってしまうため、インタラクション表示前に該当するクラスを削除します。
また、isAnimatingでアニメーション実行中のフラグを用意します。アニメーション実行中の場合は、setTimeoutを設定し、連続操作用に再度インタラクション表示の関数を呼び出すようにします。
ここでsetTimeoutを用意するのは、クラス名の削除と、再度インタラクションを呼び出した際のクラス名を追加するタイミングが、同じにならないようにするためです。CSSのanimationプロパティは、クラス名が削除されるタイミングでアニメーションがリセットされるはずですが、同タイミングで再びクラスが追加されることで、アニメーションをリセットするタイミングがなくなり、意図せずアニメーションが継続するようになってしまいます。setTimeoutを用いることで、クラス名の削除から追加までに僅かな遅延を入れ、連続操作時にアニメーションが継続しないようにします。

// アニメーション実行時用のフラグを用意
let isAnimating = false;

function startInteraction() {

  // アニメーション実行中に操作が行われた時(連続操作時)の処理
  if (isAnimating) {
    // インタラクションを再度呼び出すため、クラスを削除し、フラグを下ろす
    this.playerEl.classList.remove('animating');
    isAnimating = false;
    // 遅延後に、再びインタラクション表示処理を呼び出す
    setTimeout(() => {
      this.startInteraction();
    });
    return;
  }

  this.playerEl.classList.add('animating');
  // アニメーション実行時用のフラグを立てる
  isAnimating = true;

  this.playerEl.onanimationend = () => {
    this.playerEl.classList.remove('animating');
    // アニメーションが終了しているので、フラグを下ろす
    isAnimating = false;
  }
}

これで、連続操作に合わせてインタラクションが表示できるようになりました。

シークバーのグラデーションの実装

続いて、シークバーのグラデーションの実装についてです。現在のULIZAのブランドカラーに合わせて、シークバーのプログレス部分をグラデーションできるように実装を行いました。
他サービスを見てもシークバーがグラデーションしている類例は見ないので、独自性の高いものになったのではないかと思います。

CSSの設定

これまで、シークバーのプログレス部分は、backgroud-colorで色を指定していましたが、グラデーションが指定できるlinear-gradientを使用したいので、backgroundプロパティに変更します。
また、これまではシークバーのプログレス部分の幅をwidthで設定していましたが、widthで設定するとシークバー始点から再生位置までのグラデーションになり、再生位置によってグラデーションの幅が伸び縮みしてしまいます。グラデーション幅はシークバーの始点から終点までを想定していますので、シークバーのプログレス部分の幅の設定方法を変更します。
widthは100%固定にし、グラデーション幅がシークバーの始点から終点までになるようにし、シークバー始点から再生位置まででトリミングをpadding-rightプロパティとbackgroud-clipプロパティで行います。

.seek-progress-bar {
  /*JavaScriptで再生位置を計算した値を入れる変数を用意。値は随時更新される。*/
  --progress: 0%;

  padding-right: calc(100% - var(--progress));
  background: linear-gradient(90deg, rgba(0, 183, 228, 1) 0%, rgba(0, 0, 174, 1) 100%);
  background-clip: content-box;
}

これによってグラデーション幅がシークバーの始点から終点までになり、シークバーのグラデーションが可能になりました。
実際にグラデーション設定したシークバーは以下のようになります。

シークバーのグラデーション設定

まとめ

今回はプレイヤーのUIに関する一部のCSS, JavaScriptの実装についてまとめさせていただきました。
何かのお役に立てれば幸いです。

*1:当初、setTimeoutを用いてanimation-durationと同時間を指定する方法を用いていたのですが、onanimationendを使用した方が、CSS変更時にJavaScriptを変更する必要がない、よりアニメーション終了とタイミングが揃いやすいとご助言いただき、onanimationendを使用するようにしました。