私が輝く、夏がはじまる。
私が輝く、夏がはじまる。
2020.03.02
#209
それいけ!フロントエンド

コインのようにくるくる回転するフリップアニメーションを作ってみよう【いろんな動きを作ってみようシリーズ】

はっちゃん

こんにちは、フロントエンドエンジニアのはっちゃんです。

今日は「スマホやタブレットで見たときだけ、コインがフリップでめくれるようなアニメーション」を作っていきたいと思います。

前回の記事と組み合わせると、LIGブログのメンバーページと同じ仕様になります。

画像を用意

前回同様、表と裏の画像を用意します。


HTML

前回のHTMLを少しだけ修正します。JSのトリガーとなるclassを追加します。

<div class="flip js-flip"><!-- js-flipを追加 -->
  <div class="flip-inner js-flip-inner"><!-- js-flip-innerを追加 -->
    <div class="front">
      <img class="thumbnail" src="https://82mou.github.io/src/images/coin-omote.jpg" alt="コイン表">
    </div>
    <div class="back">
      <img class="thumbnail thumbnail-hover" src="https://82mou.github.io/src/images/coin-ura.jpg" alt="コイン裏">
    </div>
  </div>
</div>

CSS

CSSも修正します。hoverではなく、classの付け替えて回転するような仕組みにします。

もともと.flip-innerにデフォルトでついていたtransition: transform .8s ease;は、特定のclassが付いたときのみ適用されるようにします。

.flip {
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  width: 300px;
  height: 300px;
  &.is-pc {
    .flip-inner {
      transition: transform .8s ease;
    }
    &.is-hover {
      .flip-inner {
        transform: rotateY(180deg);
      }
    }
  }
  &.is-animating {
    .flip-inner {
      transition: transform .8s ease;
    }
  }
  /*&:hover {*/
  /*  .flip-inner {*/
  /*    transform: rotateY(180deg);*/
  /*  }*/
  /*}*/
  .flip-inner {
    transform-style: preserve-3d;
    transform: rotateY(0deg);
    height: 100%;
    width: 100%;
    .front, .back {
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      backface-visibility: hidden;
      border-radius: 50%;
      overflow: hidden;
    }
    .front {
      transform: translateZ(1px);
    }
    .back {
      transform: rotateY(180deg);
    }
    .thumbnail {
      width: 100%;
    }
  }
}

JS

// UA判定のJSライブラリーを実行
const uAParser = UAParser();

const $flip = $('.js-flip');
const $flipInner = $('.js-flip-inner');
const flipWidth = $flip.width();

// 円の直径を180度に割り当てる
const rate = 180 / flipWidth;

// .flip-innerクラスにつけるtransitionのduration値
const DURATION = 800;

// touchMoveのフラグ
let moving = false;

// touchEnd後、アニメーションしているときのフラグ
let animating = false;

// タップし始めたときのx座標
let startPointX = 0;

// タップを離したときのx座標
let endPointX = 0;

// 現在の回転量
let currentRotationVal = 0;

// デバイスを判定
let judgeDevice = () => {
  (uAParser.device.type !== 'mobile') ? $flip.addClass('is-pc') : $flip.removeClass('is-pc');
};

// スワイプしはじめたとき
let touchStartHandler = (e) => {
  if (uAParser.device.type !== 'mobile') return;
  if (moving) return;
  let touch = e.originalEvent.touches[0];
  // 触り始めたときのタップしたx,y座標を保存
  startPointX = touch.screenX;
};

// スワイプ中
let touchMoveHandler = (e) => {
  if (uAParser.device.type !== 'mobile') return;
  if (animating) return;
  moving = true;
  let touch = e.originalEvent.touches[0];

  // スワイプ量を保存
  endPointX = (touch.screenX - startPointX) * rate;

  // 半回転以上回転させないように-180度以上、180度以下に制限
  if (endPointX > 180 || endPointX < -180) {
    return;
  }

  roll(currentRotationVal + endPointX);
};

// 指を離したとき
let touchEndHandler = () => {
  if (uAParser.device.type !== 'mobile') return;
  if (!moving || animating) {
    return;
  }
  moving = false;

  // タップを離したとき、プラス方向に半分よりめくれていたら最後までめくる
  if(currentRotationVal + endPointX > currentRotationVal + 90) {
    currentRotationVal = currentRotationVal + 180;
  }
  // タップを離した時、マイナス方向に半分以上めくれていたら最後までめくる
  if(currentRotationVal + endPointX < currentRotationVal - 90) {
    currentRotationVal = currentRotationVal - 180;
  }

  // transitionしつつめくる
  $flip.addClass('is-animating');
  $flipInner.css('transform', 'rotateY(' + currentRotationVal + 'deg)');
  animating = true;

  setTimeout(() => {
    // cssで設定したduration分動いたら、transitionを切る
    $flip.removeClass('is-animating');
    animating = false;
  }, DURATION);

  // スワイプし始めた位置を初期化
  startPointX = 0;
};

// 回転
let roll = (rotationVal) => {
  $flipInner.css('transform', 'rotateY(' + (rotationVal) + 'deg)');
};

// リセット用イベント
$(window).on('resize', judgeDevice);

// イベント
$flip.on('touchstart', touchStartHandler);
$flip.on('touchmove', touchMoveHandler);
$flip.on('touchend', touchEndHandler);
$flip.on('mouseover', (e) => {
  if ($(e.currentTarget).hasClass('is-pc')) {
    $(e.currentTarget).addClass('is-hover');
  }
});

$flip.on('mouseout', (e) => {
  if ($(e.currentTarget).hasClass('is-pc')) {
    $(e.currentTarget).removeClass('is-hover');
  }
});

// 初期化
judgeDevice();

コードの説明

コード内に細かく機能説明は記載しましたが、最初は下記のように大枠から考えました。

あとはそれに沿って具体的な処理を書いていけばOKです。

  1. デバイス判定でスワイプをSPのときのみに限定する
  2. 触ったときのイベントの設定(touchStart)
    • 触り始めたときの座標を保存
  3. 動かしているときのイベントの設定(touchMove)
    • スワイプの量を保存
    • 現在の回転量にスワイプの量を加算し、回転させる
  4. 離したときのイベントの設定(touchEnd)
    • 離したときのスワイプ量から、めくれる方向を判定
    • 判定した方向に、めくりきれる量を回転させる
  5. hover時のイベントをPCのときのみに限定(mouseover、mouseout)

デモ

※ スマホ、タブレットで見て開き、画像を指で横になぞってみてください。

See the Pen
coin rotation animation on the flip
by k_hatsushi (@hatsushi_kazuya)
on CodePen.

まとめ

指の動きと連動させることで、よりリアルに表現できました。

細かいところですが、実際の動きに近づけるとそれだけでリッチに感じますね。

みなさんもぜひ遊んでみてくださいね!