もう普通じゃつまらない!CSSとJavaScriptで作る3Dカルーセルのアイデア

2017/08/21

Giulio Mainardi

98

Articles in this issue reproduced from SitePoint
Copyright © 2017, All rights reserved. SitePoint Pty Ltd. www.sitepoint.com. Translation copyright © 2017, KADOKAWA ASCII Research Laboratories, Inc. Japanese syndication rights arranged with SitePoint Pty Ltd, Collingwood, Victoria,Australia through Tuttle-Mori Agency, Inc., Tokyo

複数の画像を切り替えて表示するカルーセルパネルといえば、どんなサイトでも使われる定番の表現方法。ちょっと工夫して、CSSとJavaScriptで魅力的な3Dカルーセルを実装する方法を解説します。

Smashing Magazineの記事など、伝統的な2Dカルーセルの使い方についての記事はたくさんあります。「カルーセルを使うべきか」という問いに対して単純にイエスかノーでは答えられないでしょう。答えは状況によって変わります。

このトピックの調査を始めた時は、3Dカルーセルの必要性は感じないものの、実装に関する技術的な関心は高まりました。主に使われるのはCSS Transforms Module Level 1のテクニックですが、CSSやSass、クライアントサイドのJavaScriptにおける機能に関連した一連のフロントエンド開発テクノロジーにも使われます。

こちらのCodePenは、これから作成方法を説明するコンポーネントのバリエーションです

最初に、CSSでの3D変形の設定を説明するため、CSSのみで作成したコンポーネントを示します。次に、シンプルなコンポーネントのスクリプトでJavaScriptで拡張する方法を示します。

カルーセルのマークアップ

マークアップでは、以下のようにコンポーネント内の画像を<figure>要素内にラップします。基本的な骨組みです。

<div class="carousel">
  <figure>
    <img src="..." alt="">
    <img src="..." alt="">
    ...
    <img src="..." alt="">
  </figure>
</div>

これが出発点です。

カルーセルの幾何学的構造

CSSの説明に入る前に、以下のセクションで作成するプランを概観します。

<img>要素をカルーセルの輪郭となる円に沿って配置します。この円は正多角形で近似でき、画像は多角形の辺上に配置します。

Showing the theta angle

次の多角形の辺の数がカルーセルでの画像の数になります。画像が3つの場合は正三角形、4つでは正方形、5つでは正五角形、という具合です。

カルーセルの画像が3つ未満なら、多角形を定義することも、これから説明する手順を適用することもできません。画像が1つだけなのは無意味です。画像が2つならいくらか現実的で、その場合画像を円の直径の両端に向かい合うように配置できるでしょう。分かりやすくするため、特殊なケースは取り上げず「画像は3つ以上」の前提で進めます。ただし、コードを修正して対応するのは難しくはないでしょう。

基準の仮想的な多角形を3D空間に置き、辺心距離、つまり正多角形の中心から一辺までの最短距離だけ、多角形の中心をビューポート面に垂直に、画面の奥に向かって押し込みます。カルーセルを上から見た図は次のようになります。

Carousel setup top view

上の図で画面を見ている人(viewer)の正面の辺は画面上でz = 0の位置です。この正面画像は奥行き(perspective)による遠近法の影響がなく、普通の2Dサイズです。上の図の「d」はCSSのperspectiveプロパティの値を表します。

カルーセルのジオメトリ―を作成

カギになるCSSルールをステップごとに詳しく説明します。

以下のコードスニペットでは、Sass変数を使ってコンポーネントを設定しやすくしています。カルーセル内の画像数を$nで示し、画像幅の指定に$item-widthを使います。

<figure>要素は最初の画像を包含するボックスで、ほかの画像を配置・変形する基準です。カルーセルのショーケースに画像が1つ入ったところで、サイズと配置を設定します。

.carousel {
  display: flex;
  flex-direction: column;
  align-items: center;
   > * {
     flex: 0 0 auto;
  }

  .figure {
    width: $item-width;
    transform-style: preserve-3d;
    img {
      width: 100%;
      &:not(:first-of-type) {
        display: none /* Just for now */
      }
    }
  }
}

<figure>要素は前述のカルーセルアイテムの幅で、画像の高さです(サイズは変更できますが、アスペクト比は同一にします)。カルーセルコンテナの高さは画像の高さが自動的に適用されます。<figure>要素はカルーセルコンテナ内で横方向の中央揃えです。

目的のカルーセルの正面にきているので、最初の画像をさらに変形する必要はありません。

<figure>要素に回転を適用すると、カルーセルを3D空間で回転できます。多角形の中心を軸にして回転させるので、<figure>の「transform origin」のデフォルト値を変更します。

.carousel figure {
  transform-origin: 50% 50% (-$apothem);
}

この値が負になっているのは、CSSではz軸の正方向は画面からユーザー(前の図の「viewer」)の方向に伸びているからです。構文エラー対策のため、Sassでカッコでくくります。多角形の辺心距離の計算はあとで説明します。

基準の<figure>要素を記述したのち、新しくy軸を設定すればカルーセル全体が回転します。

.carousel figure {
  transform: rotateY(/* some amount here */rad);
}

この回転はあとで詳しく説明します。

ほかの画像の変形の話を進めます。次のコードでは画像を「position:absolute」に設定して<figure>内に置きます。

.carousel figure img:not(:first-of-type) {
  position: absolute;
  left: 0;
  top: 0;
}

続く変形の予備的なステップに過ぎないため、z-indexの値は記述されません。実際、この時点では、どの画像もカルーセルのy軸で回転できます。回転角は画像が配置される多角形の辺によって決まります。<figure>要素と同様、画像のデフォルトの「transform origin」を変更して多角形の中央に移動します。

.img:not(:first-of-type) {
  transform-origin: 50% 50% (-$apothem);
}

次に($i - 1) * $thetaで与えられる角度(単位はラジアン)でy軸に沿って画像を回転できます。$iは画像のインデックス(1始まり)です。$theta = 2 * $PI / $nであり、$PIは数学定数pi(パイ)を表します。したがって2番目の画像の回転角は$theta、3番目の画像の回転角は2 * $thetaとなり、最後の画像は($n - 1) * $thetaだけ回転します。

Carousel polygon

ネストされたCSStransformの階層構造により、画像の相対配置はカルーセルが回転している間(設定されたy軸周りで<figure>が回転している間)保たれます。

画像ごとの回転量はSassのコントロールディレクティブ@forで設定します。

.carousel figure img {
  @for $i from 2 through $n {
    &:nth-child(#{$i}) {
      transform: rotateY(#{($i - 1) * $theta}rad);
    }
  }
}

for...toは、インデックス変数$iに割り当てられる最後の値がnではなくn-1になるため、構文のfor...toではなくfor...throughを使います。

Sassの#{}でくくられた構文には2つのインスタンスが入ります。最初のインスタンスは、:nth-child()セレクタのインデックスとして使い、2番目のインスタンスはrotationプロパティの値の設定に使います。

辺心距離の計算

多角形の辺心距離の計算結果は辺の数と辺の長さ、つまり変数$n(辺の数)と$item-width(辺の長さ)の値で決まります。は次のとおりです。

$image-width / (2 * tan($PI/$n))

tan()三角関数のタンジェント(tangent)です。

この式は幾何学と三角法から導きます。CodePenのソースコードに式が記述していません。Sassでタンジェント関数を使うのは難しいので、代わりにハードコードされた値を使います。JavaScriptのデモには完全な形で実装します。

カルーセルアイテムに余白を設定

カルーセル画像は横並びに「つなぎ合わされ」、所定の多角形状を形成します。画像はすき間なく並んでいますが、3Dカルーセルでは背景に裏向きの画像を可視化し見栄えをよくするため画像間に余白を取ります。

画像間の余白は、別の設定変数$item-separationで、各<img>要素の横方向のpaddingで追加できます。精度を増すには、この値を2で割って左右のpaddingに設定します。

.carousel figure img {
  padding: 0 $item-separation / 2;
}

最終結果を以下のデモで確認できます。

opacityプロパティによって画像を半透明にしているので、カルーセル構造がより分かりやすく示されています。カルーセルのルート要素にflexレイアウトを使ったことで、カルーセルがビューポート内で縦方向に中央揃えされています。

カルーセルの回転

カルーセルの回転を簡単にテストするために、画像を進めたり戻したりするUIコントロールを追加します。コントロールを実装するHTML、CSS、JavaScriptはCodePenのデモで確認してください。ここでは回転に関するコードについてのみ説明します。

整数型(integer)の変数currImageで、どの画像がカルーセルの正面にくるかを示します。ユーザーが「前へ(Prev)/次へ(Next)」ボタンを操作すると、1つのユニットの中で変数がインクリメントしたりデクリメントしたりします。

currImageを追加した、カルーセルの回転に関するコードです。

figure.style.transform = `rotateY(${currImage * -theta}rad)`;

(このコードやこれから紹介するスニペットではES6テンプレートリテラルが使われ、文字列内挿法で表現されています。伝統的な連結演算子「+」でも大丈夫です)

thetaは先ほどのコードと同様です。

numImages = figure.childElementCount;
theta =  2 * Math.PI / numImages;

回転を- thetaに設定するのは、反時計回りに回転するように次のアイテムをナビゲートするためです。CSSのtransformプロパティは、この場合rotateの値を負で設定します。

currImageの値は [0, numImages – 1] の範囲に限定されるわけではなく、正方向や負方向に無限に増加させることも可能です。最後の画像が正面にきているとき(つまりcurrImage == n-1)にユーザーが「次へ」ボタンをクリックすると、currImage0にリセットして最初のカルーセルイメージに進み、回転角が(n-1)*thetaから0に移行し、結果、それまでのすべての画像が逆向きに進むようにカルーセルが回転します。同じ問題は、最初の画像が正面にきているときに「前へ」ボタンがクリックされたときにも起こります。

厳密に言うと、データタイプNumberは制限があるため、currentImageの潜在的なオーバーフローもチェックします。このチェックはデモのコードに実装していません。

以下に回転カルーセルを示します。

JavaScriptによる拡張

カルーセルの主要部分の基本的なCSSを確認したところで、今度はJavaScriptを使ってコンポーネントを拡張します。次の方法があります。

  • 画像数を調整
  • 画像幅をパーセント指定
  • ページに複数のカルーセルインスタンスを実装
  • 余白のサイズや(画像の)裏面の見せ方などインスタンスごとの設定
  • HTML5のdata-*属性を使った設定

スタイルシートから「transform origin」と回転(rotation)関連の変数とルールを削除します。JavaScriptで次のように設定します。

$item-width: 40%; // Now we can use percentages
$item-separation: 0px; // This now is set with Js
$viewer-distance: 500px;

.carousel {
  padding: 20px;

  perspective: $viewer-distance;
  overflow: hidden;

  display: flex;
  flex-direction: column;
  align-items: center;
  > * {
    flex: 0 0 auto;
  }

  figure {
    margin: 0;
    width: $item-width;

    transform-style: preserve-3d;

    transition: transform 0.5s;

    img {
      width: 100%;
      box-sizing: border-box;
      padding: 0 $item-separation / 2;

      &:not(:first-of-type) {
        position: absolute;
        left: 0;
        top: 0;
      }
    }
  }
}

次のスクリプトは、インスタンスの初期化関連の関数carousel()です。

function carousel(root) {
  // coming soon...
}

引数rootはカルーセルを包含するDOM要素を表します。

この関数はコンストラクタで、ページ上で各カルーセル用にオブジェクトを1つ生成します。この記事ではカルーセルライブラリーについて取り上げないので、シンプルな関数で十分です。

同一ページ上でいくつかのコンポーネントをインスタンス化するために、loadイベント用のwindowオブジェクトにリスナーを登録して、すべての画像がロードされるのを待ってから要素ごとにcarouselクラスでcarousel()を呼び出します。

window.addEventListener('load', () => {
  var carousels = document.querySelectorAll('.carousel');

  for (var i = 0; i < carousels.length; i++) {
    carousel(carousels[i]);
  }
});

関数carousel()は次の3つの主要なタスクを実行します。

  • ナビゲーションの設定。コードは2番目のCodePenデモと同じ
  • transformの設定
  • 「window resize」のリスナーを登録してカルーセルをレスポンシブ対応にしておき、カルーセルに新しいビューポートサイズを適用

transformの設定コードを試す前に、主要な変数のいくつかと、インスタンスの設定に基づき、どう初期化されるのか説明します。

var
  figure = root.querySelector('figure'),
  images = figure.children,
  n = images.length,
  gap = root.dataset.gap || 0,
  bfc = 'bfc' in root.dataset
;

画像数(n)は<figure>要素の子要素の数に基づき初期化します。スライド間に間隔(gap)が設定される場合、HTML5のdata-gap属性で初期化します。要素の裏面の可視化に関するフラグ(bfc)は、HTML5のデータセットAPIを使って読み出します。カルーセルの背景に画像が見えるか決定するためあとで使います。

CSS Transformの設定

CSSのtransform関連プロパティを設定するコードはsetupCarousel()内にカプセル化します。ネストされたこの関数は引数を2つ取り、最初の引数はカルーセル内のアイテム数、つまり前述の変数で、2番目のパラメータは、カルーセルの多角形の辺の長さです。画像幅と等しいので、辺の長さのうちの1つをgetComputedStyle()で読み出します。

setupCarousel(n, parseFloat(getComputedStyle(images[0]).width));

この方法で画像幅をパーセント値で設定できます。

カルーセルをレスポンシブ対応にするため、window resizeイベント用にリスナーを登録し、画像のサイズ変更に合わせて再度setupCarousel()を呼び出します。

window.addEventListener('resize', () => { 
  setupCarousel(n, parseFloat(getComputedStyle(images[0]).width));
});

シンプルにするために「resize」リスナーの絞り込みは実装していません。

setupCarousel()が実行する最初の動作は多角形の辺心距離の計算です。渡されるパラメータと先に説明した式で実行します。

apothem = s / (2 * Math.tan(Math.PI / n));

この値は「figure」要素の「transform origin」の変更に使われ、カルーセルの新しい回転軸を取得します。

figure.style.transformOrigin = `50% 50% ${-apothem}px`;

画像のスタイルを適用します。

for (var i = 0; i < n; i++) {
  images[i].style.padding = `${gap}px`;
}

for (i = 1; i < n; i++) {
  images[i].style.transformOrigin = `50% 50% ${- apothem}px`;
  images[i].style.transform = `rotateY(${i * theta}rad)`;
}

if (bfc) {
  for (i = 0; i < n; i++) {
    images[i].style.backfaceVisibility = 'hidden';
  }
}

最初のサイクルではカルーセルアイテム間の余白にpaddingを設定しています。2番目のサイクルでは3D transformを設定しています。最後のサイクルでは、カルーセルに「back-face」(裏面)関連フラグを設定している場合、画像の裏面の可視化に関する処理をします。

最後に、rotateCarousel()を呼び出すと現在の画像が正面にきます。ちょっとしたヘルパー関数で、表示する画像のインデックスが与えられると、figure要素をy軸で回転させて目的の画像を正面に移動します。画像を進めたり戻したりするためのナビゲーションのコードにも使われます。

function rotateCarousel(imageIndex) {
  figure.style.transform = `rotateY(${imageIndex * -theta}rad)`;
}

以下に最終結果のデモを示します。カルーセル例がいくつか示されていますが、設定はそれぞれ異なります。

参考資料

このチュートリアルで調査に使った資料をいくつか紹介します。

(原文:Building a 3D Rotating Carousel with CSS and JavaScript

[翻訳:新岡祐佳子/編集:Livit

Copyright © 2017, Giulio Mainardi All Rights Reserved.

 Giulio Mainardi

Giulio Mainardi

フロントエンド開発について勉強中です。

Loading...