【CSSアニメーション】CSSオンリーの分割型スライダー

こんにちは、牛尾です。

数年前に実験的に実装した「CSSオンリースライダー」のコードを発見したので、改めて発表したいと思います(当時作成した社内記事の転用です)。
こんなスライダー、あまり実装することないと思いますがw

名を「スライドスプリット」としました。

仕様・要件

画像が分割したような状態で変化するスライダー
  • 1セットの中で分割する数(ブロック数)を変更可能
  • セットの数を増減しても動作(最低2セット以上)
  • ブロックのエフェクトを自由に指定(transformなどのCSSアニメ系プロパティを使用)
  • エフェクトの時間を自由に指定(秒)
  • ブロックごとのエフェクトの実行間隔を自由に設定(秒)でき、前のエフェクトが終わる前に次のエフェクトも実行可能
  • 表示時と各セット間で一時停止時間を自由に設定(秒)
  • ブロックの中は画像以外でも入れられるように、背景画像にしない(エフェクトの実行はブロックごと行う)
  • レスポンシブ対応
  • 横幅と横位置を自動計算
以上のような仕様を選定してロジックを考えることにしました。
残念ながら、全てのブロックを「position: releative;」で浮かせているので、高さを%などで指定しなくてはならなくなっています。

タイムライン

以下のような流れがタイムラインになります。

ざっくり書くと
エフェクト → 停止 → エフェクト前の状態に戻る → 停止 ・・・
というようなタイムラインを1つ作り、それを一定間隔でずらして実行する、という考え方です。
(CSSのコード的には、keyframesとanimation-delayを使います)

例えば1セットあたりのブロック数が5個だった場合、位置的に6番目のブロックのエフェクトが完了したと同時に、1番目のブロックがエフェクト前の状態に戻るようにタイミングを合わせる必要があります。

また、アニメーションのブロックごとのズレには、セット間の停止時間も含めなくてはならないので、上記例で言うと、6番目のエフェクトが実行される直前に、セット間の停止時間を足す必要があります。
これを1セットあたりのブロック数(例の場合、5個に1回)ごとに行う必要があります。

つまり、エフェクトの実行間隔が1秒の場合
0s → 1s → 2s → 3s → 4s → (一時停止) → 10s → 11s ・・・
という風にズラす必要があります。
更に厳密に言うと、1セット目は既に定位置についた状態から始まるので、例で言うと6番目の実行が一時停止後から始まるように、1〜5番目の実行タイミングをマイナスになるようにもしなくてはなりません。

コード

HTML

    <div class='container slider1'>
        <p>
            効果:上から下に & フェード<br>
            セットあたりのブロック数:5<br>
            セット数:3(黄→青→赤)<br>
            効果時間:1秒<br>
            効果発動の間隔:0.5秒<br>
            セット間の停止時間:3秒
        </p>
        <div class='slider_split clearfix'>
            <div class='slider_split_inner slider_split_1'>
                <img src='https://placehold.jp/ffc/888/240x400.png?text=1' alt="">
            </div>
            <div class='slider_split_inner slider_split_2'>
                <img src='https://placehold.jp/ffc/888/240x400.png?text=2' alt="">
            </div>
            <div class='slider_split_inner slider_split_3'>
                <img src='https://placehold.jp/ffc/888/240x400.png?text=3' alt="">
            </div>
            <div class='slider_split_inner slider_split_4'>
                <img src='https://placehold.jp/ffc/888/240x400.png?text=4' alt="">
            </div>
            <div class='slider_split_inner slider_split_5'>
                <img src='https://placehold.jp/ffc/888/240x400.png?text=5' alt="">
            </div>
            <div class='slider_split_inner slider_split_6'>
                <img src='https://placehold.jp/cff/888/240x400.png?text=6' alt="">
            </div>
            <div class='slider_split_inner slider_split_7'>
                <img src='https://placehold.jp/cff/888/240x400.png?text=7' alt="">
            </div>
            <div class='slider_split_inner slider_split_8'>
                <img src='https://placehold.jp/cff/888/240x400.png?text=8' alt="">
            </div>
            <div class='slider_split_inner slider_split_9'>
                <img src='https://placehold.jp/cff/888/240x400.png?text=9' alt="">
            </div>
            <div class='slider_split_inner slider_split_10'>
                <img src='https://placehold.jp/cff/888/240x400.png?text=10' alt="">
            </div>
            <div class='slider_split_inner slider_split_11'>
                <img src='https://placehold.jp/fcc/888/240x400.png?text=11' alt="">
            </div>
            <div class='slider_split_inner slider_split_12'>
                <img src='https://placehold.jp/fcc/888/240x400.png?text=12' alt="">
            </div>
            <div class='slider_split_inner slider_split_13'>
                <img src='https://placehold.jp/fcc/888/240x400.png?text=13' alt="">
            </div>
            <div class='slider_split_inner slider_split_14'>
                <img src='https://placehold.jp/fcc/888/240x400.png?text=14' alt="">
            </div>
            <div class='slider_split_inner slider_split_15'>
                <img src='https://placehold.jp/fcc/888/240x400.png?text=15' alt="">
            </div>
        </div>
    </div>

    <div class='container slider2'>
        <p>
            効果:フェード<br>
            セットあたりのブロック数:4<br>
            セット数:4(黄→青→赤→紫)<br>
            効果時間:2秒<br>
            効果発動の間隔:2秒<br>
            セット間の停止時間:0秒
        </p>
        <div class='slider_split clearfix'>
            <div class='slider_split_inner slider_split_1'>
                <img src='https://placehold.jp/ffc/888/300x400.png?text=1' alt="">
            </div>
            <div class='slider_split_inner slider_split_2'>
                <img src='https://placehold.jp/ffc/888/300x400.png?text=2' alt="">
            </div>
            <div class='slider_split_inner slider_split_3'>
                <img src='https://placehold.jp/ffc/888/300x400.png?text=3' alt="">
            </div>
            <div class='slider_split_inner slider_split_4'>
                <img src='https://placehold.jp/ffc/888/300x400.png?text=4' alt="">
            </div>
            <div class='slider_split_inner slider_split_5'>
                <img src='https://placehold.jp/cff/888/300x400.png?text=5' alt="">
            </div>
            <div class='slider_split_inner slider_split_6'>
                <img src='https://placehold.jp/cff/888/300x400.png?text=6' alt="">
            </div>
            <div class='slider_split_inner slider_split_7'>
                <img src='https://placehold.jp/cff/888/300x400.png?text=7' alt="">
            </div>
            <div class='slider_split_inner slider_split_8'>
                <img src='https://placehold.jp/cff/888/300x400.png?text=8' alt="">
            </div>
            <div class='slider_split_inner slider_split_9'>
                <img src='https://placehold.jp/fcc/888/300x400.png?text=9' alt="">
            </div>
            <div class='slider_split_inner slider_split_10'>
                <img src='https://placehold.jp/fcc/888/300x400.png?text=10' alt="">
            </div>
            <div class='slider_split_inner slider_split_11'>
                <img src='https://placehold.jp/fcc/888/300x400.png?text=11' alt="">
            </div>
            <div class='slider_split_inner slider_split_12'>
                <img src='https://placehold.jp/fcc/888/300x400.png?text=12' alt="">
            </div>
            <div class='slider_split_inner slider_split_13'>
                <img src='https://placehold.jp/ccf/888/300x400.png?text=13' alt="">
            </div>
            <div class='slider_split_inner slider_split_14'>
                <img src='https://placehold.jp/ccf/888/300x400.png?text=14' alt="">
            </div>
            <div class='slider_split_inner slider_split_15'>
                <img src='https://placehold.jp/ccf/888/300x400.png?text=15' alt="">
            </div>
            <div class='slider_split_inner slider_split_16'>
                <img src='https://placehold.jp/ccf/888/300x400.png?text=15' alt="">
            </div>
        </div>
    </div>

    <div class='container slider3'>
        <p>
            効果:540度 縦回転<br>
            セットあたりのブロック数:5<br>
            セット数:3(黄→青→赤)<br>
            効果時間:1秒<br>
            効果発動の間隔:0.25秒<br>
            セット間の停止時間:2秒
        </p>
        <div class='slider_split clearfix'>
            <div class='slider_split_inner slider_split_1'>
                <img src='https://placehold.jp/ffc/888/240x400.png?text=1' alt="">
            </div>
            <div class='slider_split_inner slider_split_2'>
                <img src='https://placehold.jp/ffc/888/240x400.png?text=2' alt="">
            </div>
            <div class='slider_split_inner slider_split_3'>
                <img src='https://placehold.jp/ffc/888/240x400.png?text=3' alt="">
            </div>
            <div class='slider_split_inner slider_split_4'>
                <img src='https://placehold.jp/ffc/888/240x400.png?text=4' alt="">
            </div>
            <div class='slider_split_inner slider_split_5'>
                <img src='https://placehold.jp/ffc/888/240x400.png?text=5' alt="">
            </div>
            <div class='slider_split_inner slider_split_6'>
                <img src='https://placehold.jp/cff/888/240x400.png?text=6' alt="">
            </div>
            <div class='slider_split_inner slider_split_7'>
                <img src='https://placehold.jp/cff/888/240x400.png?text=7' alt="">
            </div>
            <div class='slider_split_inner slider_split_8'>
                <img src='https://placehold.jp/cff/888/240x400.png?text=8' alt="">
            </div>
            <div class='slider_split_inner slider_split_9'>
                <img src='https://placehold.jp/cff/888/240x400.png?text=9' alt="">
            </div>
            <div class='slider_split_inner slider_split_10'>
                <img src='https://placehold.jp/cff/888/240x400.png?text=10' alt="">
            </div>
            <div class='slider_split_inner slider_split_11'>
                <img src='https://placehold.jp/fcc/888/240x400.png?text=11' alt="">
            </div>
            <div class='slider_split_inner slider_split_12'>
                <img src='https://placehold.jp/fcc/888/240x400.png?text=12' alt="">
            </div>
            <div class='slider_split_inner slider_split_13'>
                <img src='https://placehold.jp/fcc/888/240x400.png?text=13' alt="">
            </div>
            <div class='slider_split_inner slider_split_14'>
                <img src='https://placehold.jp/fcc/888/240x400.png?text=14' alt="">
            </div>
            <div class='slider_split_inner slider_split_15'>
                <img src='https://placehold.jp/fcc/888/240x400.png?text=15' alt="">
            </div>
        </div>
    </div>

    <div class='container slider4'>
        <p>
            効果:720度回転 & 3倍→1倍に縮小<br>
            セットあたりのブロック数:5<br>
            セット数:3(黄→青→赤)<br>
            効果時間:1秒<br>
            効果発動の間隔:0.25秒<br>
            セット間の停止時間:2秒
        </p>
        <div class='slider_split clearfix'>
            <div class='slider_split_inner slider_split_1'>
                <img src='https://placehold.jp/ffc/888/240x240.png?text=1' alt="">
            </div>
            <div class='slider_split_inner slider_split_2'>
                <img src='https://placehold.jp/ffc/888/240x240.png?text=2' alt="">
            </div>
            <div class='slider_split_inner slider_split_3'>
                <img src='https://placehold.jp/ffc/888/240x240.png?text=3' alt="">
            </div>
            <div class='slider_split_inner slider_split_4'>
                <img src='https://placehold.jp/ffc/888/240x240.png?text=4' alt="">
            </div>
            <div class='slider_split_inner slider_split_5'>
                <img src='https://placehold.jp/ffc/888/240x240.png?text=5' alt="">
            </div>
            <div class='slider_split_inner slider_split_6'>
                <img src='https://placehold.jp/cff/888/240x240.png?text=6' alt="">
            </div>
            <div class='slider_split_inner slider_split_7'>
                <img src='https://placehold.jp/cff/888/240x240.png?text=7' alt="">
            </div>
            <div class='slider_split_inner slider_split_8'>
                <img src='https://placehold.jp/cff/888/240x240.png?text=8' alt="">
            </div>
            <div class='slider_split_inner slider_split_9'>
                <img src='https://placehold.jp/cff/888/240x240.png?text=9' alt="">
            </div>
            <div class='slider_split_inner slider_split_10'>
                <img src='https://placehold.jp/cff/888/240x240.png?text=10' alt="">
            </div>
            <div class='slider_split_inner slider_split_11'>
                <img src='https://placehold.jp/fcc/888/240x240.png?text=11' alt="">
            </div>
            <div class='slider_split_inner slider_split_12'>
                <img src='https://placehold.jp/fcc/888/240x240.png?text=12' alt="">
            </div>
            <div class='slider_split_inner slider_split_13'>
                <img src='https://placehold.jp/fcc/888/240x240.png?text=13' alt="">
            </div>
            <div class='slider_split_inner slider_split_14'>
                <img src='https://placehold.jp/fcc/888/240x240.png?text=14' alt="">
            </div>
            <div class='slider_split_inner slider_split_15'>
                <img src='https://placehold.jp/fcc/888/240x240.png?text=15' alt="">
            </div>
        </div>
    </div>

HTMLは至って簡単。
これは5ブロック×3セットのコードです。
HTML上でセットの区別はないので、単純に15個分のブロックを作成しています。
実際にエフェクトするブロックのクラス名が【slider_split_XX】で連番になるようにしています。

Sass

    @mixin SSEffectBefore {
// 上から降りてくる
transform: translate3d(0,-100%,0);

// フェードしながら
opacity: 0;

// 横に回転しながら展開
//transform: rotateY(180deg);
//transform-style: preserve-3d;
//backface-visibility: hidden;
}
@mixin SSEffectAfter {
// 上から降りてくる
transform: translate3d(0,0,0);

// フェードしながら
opacity: 1;

// 横に回転しながら展開
//transform: rotateY(0deg);
//transform-style: preserve-3d;
//backface-visibility: hidden;
}
$SSName: SS1; // キーフレームの名前
$SSHeight: 33.3333%; // スライダーの高さ(絶対値 or 横幅に対する%)
$SSSetInner: 5; // 1セットあたりのブロック数
$SSSetCount: 3; // セットの数
$SSSetEffect: 1; // 変化時間
$SSSetTiming: .5; // 次の変化を実行する時間
$SSSetStay: 3; // セット間の停止時間
$SSInfinite: infinite; // アニメーションの繰り返し(infinite or 数値)
$SSEasing: ease-out; // アニメーションの進行割合(ease linear ease-in ease-out ease-in-out cubic-bezier(数値, 数値, 数値, 数値))

$SSCount: $SSSetCount * $SSSetInner; // ブロックの総数

$SSSetEffectTiming: $SSSetEffect - $SSSetTiming; // 次の変化が起こるまでの差分
$SSSetMoving: ($SSSetEffect * $SSSetInner) - ($SSSetEffectTiming * ($SSSetInner - 1)); // 1セットあたりの動作時間
$SSSetTurn: $SSSetMoving + $SSSetStay; // 1ターンあたりの時間
$SSSetNext: $SSSetTurn + $SSSetEffect; // 1ターン+次の変化時間
$SSAll: $SSSetTurn * $SSSetCount; // 全体の所

$SSSetEffectRatio: $SSSetEffect / $SSAll * 100; // 変化の割合
$SSSetNextRatio: $SSSetNext / $SSAll * 100; // 1ターン+次の変化時間の割合
$SSSetInnerRatio: 100 / $SSSetInner; // ブロックあたりの横位置(%)

.slider_split {
overflow: hidden;
position: relative;
padding-top: $SSHeight;
background: #f5f5f5;
.slider_split_inner {
position: absolute;
z-index: 2;
top: 0;
width: $SSSetInnerRatio * 1%;
@include SSEffectBefore;
animation-name: $SSName;
animation-duration: $SSAll + s;
animation-iteration-count: $SSInfinite;
animation-timing-function: $SSEasing;
&:nth-child(-n+#{$SSSetInner}) {
@include SSEffectAfter;
z-index: 1;
}
@keyframes #{$SSName} {
0% {
@include SSEffectBefore;
z-index: 2;
}
#{$SSSetEffectRatio}% { //最初の変化の終わり
@include SSEffectAfter;
z-index: 2;
}
#{$SSSetNextRatio}% {
@include SSEffectAfter;
z-index: 1;
}
#{$SSSetNextRatio + 0.001}% {
@include SSEffectBefore;
z-index: 1;
}
100% {
@include SSEffectBefore;
z-index: 1;
}
}
}
}

@for $value from 1 through $SSSetInner { // ブロックの横位置を自動計算
$position: ($SSSetInnerRatio * $value) - $SSSetInnerRatio;
.slider_split {
.slider_split_inner {
&:nth-child(#{$SSSetInner}n+#{$value}) {
left: $position * 1%;
}
}
}
}

$delay: $SSSetMoving * -1;
@for $value from 1 through $SSCount {
.slider_split_#{$value} {
animation-delay: $delay + s;
}
$delay: $delay + $SSSetTiming;
@if ($value % $SSSetInner == 0){
$delay: $delay + $SSSetEffect - $SSSetTiming + $SSSetStay;
}
    }

逆にSassは非常に複雑になってます。

変数、Mixin

仕様に応じて様々なものを変更しても全ての計算が合うように、数値を変数化し、エフェクトはMixinで対応しています。
任意指定の変数の説明は割愛。コメントアウト参照のこと。

$SSSetMoving

1セットあたりのブロックが動いている時間(ムービング)。
エフェクト実行中に次のエフェクトが実行される可能性があるため、エフェクト時間($SSSetEffect)と実行タイミング($SSSetTiming)を引いた差分をブロック数分足していくわけですが、セット間の停止時間が0秒の可能性もあるため、最後のブロックはエフェクトの完了を待つ必要があるので、最後だけエフェクト時間を足すように計算式を調整しています。

$SSSetTurn

「ムービング」とセット間の停止時間(ステイ)を足した時間を「ターン」と命名。
目に見える部分では、この「ターン」がセット数分繰り返されていることになります。

$SSSetNext

ブロックのエフェクト完了時、その前の周回のブロックを初期位置に戻す必要があるのですが、それが「ターン」に1回分のエフェクト時間を足した時間になります。

$SSSetEffectRatio、$SSSetNextRatio

CSS3の【@keyframes】は、【animation-duration】プロパティで指定した時間の割合(%)でタイミングの指定をするので、【@keyframes】に必要な時間の割合を計算しています。

キーフレーム

このスライダーでもっとも重要な部分。
変数や各計算式で求められる値をタイムラインに沿って割り振っていきます。
o}% { //最初の変化の終わり
@include SSEffectAfter;
z-index: 2;
}
#{$SSSetNextRatio}% {
@include SSEffectAfter;
z-index: 1;
}
#{$SSSetNextRatio + 0.001}% {
@include SSEffectBefore;
z-index: 1;
}
100% {
@include SSEffectBefore;
z-index: 1;
}
}

3番目のキーフレームで「0.001%」を足しているのは、アニメーションしながらではなく瞬間的に初期位置に戻すためです。
※小数点3桁なのは、Edgeなどの一部ブラウザが小数点4桁以下を無視してしまうためです。

↓上記コードのCSS(コンパイル後)
@keyframes SS1 {
0% {
transform: translate3d(0, -100%, 0);
opacity: 0;
z-index: 2;
}
5.55556% {
transform: translate3d(0, 0, 0);
opacity: 1;
z-index: 2;
}
38.88889% {
transform: translate3d(0, 0, 0);
opacity: 1;
z-index: 1;
}
38.88989% {
transform: translate3d(0, -100%, 0);
opacity: 0;
z-index: 1;
}
100% {
transform: translate3d(0, -100%, 0);
opacity: 0;
z-index: 1;
}
}

横位置の自動計算

    @for $value from 1 through $SSSetInner { // ブロックの横位置を自動計算
$position: ($SSSetInnerRatio * $value) - $SSSetInnerRatio;
.slider_split {
.slider_split_inner {
&:nth-child(#{$SSSetInner}n+#{$value}) {
left: $position * 1%;
}
}
}
    }

【left: $position * 1%;】と1%を掛けているのは、単純に単位を%にしたいだけです。

↓上記コードのCSS(コンパイル後)
.slider1 .slider_split .slider_split_inner:nth-child(5n+1) {
left: 0%;
}
.slider1 .slider_split .slider_split_inner:nth-child(5n+2) {
left: 20%;
}
.slider1 .slider_split .slider_split_inner:nth-child(5n+3) {
left: 40%;
}
.slider1 .slider_split .slider_split_inner:nth-child(5n+4) {
left: 60%;
}
.slider1 .slider_split .slider_split_inner:nth-child(5n+5) {
left: 80%;
}

各ブロックのアニメーション実行時間をズラす

キーフレーム同様、もっとも重要な部分。
ブロックごとに、設定したキーフレームを実行するタイミングを【animation-delay】プロパティでズラします。
    $delay: $SSSetMoving * -1;
@for $value from 1 through $SSCount {
.slider_split_#{$value} {
animation-delay: $delay + s;
}
$delay: $delay + $SSSetTiming;
@if ($value % $SSSetInner == 0){
$delay: $delay + $SSSetEffect - $SSSetTiming + $SSSetStay;
}
    }

先に解説したとおり、1セット目は表示時にはエフェクト後の位置で必要があるので、1ターン分時間をマイナスしてから計算を始めます。

【@if】の部分は、セット間の停止時間「ステイ」を1セットごとに足す必要があるので、ループが何回目かの判定をして、「ステイ」を足しています。

↓上記コードのCSS(コンパイル後)
.slider1 .slider_split_1 {
animation-delay: -3s;
}
.slider1 .slider_split_2 {
animation-delay: -2.5s;
}
.slider1 .slider_split_3 {
animation-delay: -2s;
}
.slider1 .slider_split_4 {
animation-delay: -1.5s;
}
.slider1 .slider_split_5 {
animation-delay: -1s;
}
.slider1 .slider_split_6 {
animation-delay: 3s;
}
.slider1 .slider_split_7 {
animation-delay: 3.5s;
}
.slider1 .slider_split_8 {
animation-delay: 4s;
}
.slider1 .slider_split_9 {
animation-delay: 4.5s;
}
.slider1 .slider_split_10 {
animation-delay: 5s;
}
.slider1 .slider_split_11 {
animation-delay: 9s;
}
.slider1 .slider_split_12 {
animation-delay: 9.5s;
}
.slider1 .slider_split_13 {
animation-delay: 10s;
}
.slider1 .slider_split_14 {
animation-delay: 10.5s;
}
.slider1 .slider_split_15 {
animation-delay: 11s;
}

サンプル

https://codepen.io/YoshihiroUshio/pen/RwLgJwE

注意

古いAndroid端末では、ほぼ確実に処理落ちします。
「Google Chrome」であれば古い端末でもギリギリ動作します。
「IE11」でも動作しますが、ちょっとコマ落ちっぽくなったりしますが、処理能力のせいなので不可避。
「iOS」では、アニメーションに【transform: translate】系を使うときは「translate3d」で指定するとカクつきが解消します。

その他、実行速度や間隔が短すぎる場合、処理落ちしたり、実行誤差によっておかしな挙動に見える場合があるので、時間の調整は実機で見ながら行ったほうが良いです。

まとめ

CSSアニメーションの可能性を改めて感じましたが、これくらいタイムラインが複雑になると、キーフレームを生成するための計算式を考えることだけで物凄く頭を使います。

あとSassがないとこれは手書きではほぼ無理。
そういう意味でも良い時代にはなった。
あとは自分の頭が追いつくかどうか次第・・・。

関連記事