CSS
CSS3
scss
アニメーション

美術が「2」だったEngineerでも作れるCSS アニメーション

More than 1 year has passed since last update.

はじめに

ホットペッパー グルメで開発を担当しているSE(Standing Engineer)の小山瑞樹 (こやまみずき)です。
担当はWebアプリケーションのServer Sideの実装(Java)ですが、最近はWeb Front EndでA/Bテストの実装(JavaScript+CSS)をメインに開発しています。

さて、私の所属するチームでは普段はこの記事みたいに少しずつ美味しいものを堪能するコース料理のようなA/Bテストをたくさん回しているのですが、なんとなくホールケーキをドカ食いしてもいいんじゃないかという気がする瞬間がやってきて画面一面をジャックしてしまうA/Bテストを実施することもあります。

(例:ハロウィンの施策)

このときのアニメーションの実装がなかなか楽しかったので、作り方の整理の意味をこめてCSSで作るアニメーションの基本的なところを書いてみます。

作ってみるもの

せっかくCSSでアニメーションを作るのなら、なにかテーマを決めてみようと思います。
12月の一大イベントといえば・・・そうですね。冬至です。今年は21日みたいです。

なので、冬至にちなんだ【柚子湯】のアニメーションを作ってみます。

  1. 柚子を落として
  2. 浮かばせて
  3. 湯気

※ こんな感じを目指します。
yuzuyu_s.gif

アニメーションで使うCSSのおさらい

@keyframes

要素をどういう風に動かしていくかを設定していきます。
「どのタイミングでなにを行うか」を細かく設定していけるので、使い方次第で複雑な動きも作れます。

例えば、不透明度を徐々に増やしていき要素をゆっくり表示する場合は次のように書けます。

@keyframes fadeIn {
  0% {
    opacity: 0;
  }
  100% {
    opacity: 1;
  }
}

要素の動かし方

@keyframesに書いていくことで、要素に変化を与えられます。

transform

要素の状態をベースにして、移動・変形・拡大/縮小・反転・回転ができます。

translateY(移動する値)

縦方向に要素を移動させます。正の値で↓へ、負の値で↑へ移動します。

opacity

要素全体の不透明度を0~1の間で指定します。
0(透明)→→→→1(不透明)です。
その要素にかかれている文字や画像も含めて透明にするので、0は完全に見えません。
@keyframesで指定することで、簡単にfadeIn, fadeOutが実装できます。

アニメーションの設定

要素に設定するアニメーションのプロパティです。
複数指定する場合はカンマ区切りでa, b, cのように書きます。

animation-name

要素にかけるアニメーションの名前です。
@keyframesでつけた名前を設定してください。

animation-duration

アニメーションをどれだけの時間をかけて行うか(0%~100%までにかける時間)です。

animation-timing-function

アニメーションの進め方です。
0%→100%にどのように進んでいくかを設定できます。(最初がゆっくりで徐々にスピードアップする・・・など)

※ timing-functionの調整

ChromeのDeveloper Toolsで視覚的にいじることが可能ですので、動き方を調整する際は使ってみてください。
chromeでtiming-functionを調整.png

animation-delay

アニメーションを開始するまでの待ち時間です。
少し送らせてから実行したい場合には設定します。

animation-iteration-count

アニメーションを繰り返す回数です。
infiniteを指定すると永遠に繰り返します。

animation-direction

アニメーションの繰り返し方です。
たぶん、これだけ覚えていれば大丈夫でしょう。

  • normal 0%→100%を繰り返していきます。 (デフォルト)
  • alternate 0→100%、100%→0%を繰り返していきます。

実際に作ってみる

(0) 柚子要素を作る

丸を書いてそれっぽいのを作ります。
色を調べたりしながら、こんな感じにしてみました。
柚子.png

少しだけ楽をするために、scssに頼りました。

柚子本体
$yuzu-size: 120px;
.yuzu {
    position: fixed;
    z-index: 9999;
    top: 100vh;
    display: block;
    box-sizing: border-box;
    background-color: hsl(60, 100%, 73%);
    border-radius: 50%;
    width: $yuzu-size;
    height: $yuzu-size;
    box-shadow: 30px 30px 16px hsla(0, 0%, 0%, 0.2);
    &::before, &::after {
        content: "";
        border-radius: 50%;
        box-sizing: border-box;
        position: absolute;
    }
    &::before {
        background-color: transparent;
        width: $yuzu-size;
        height: $yuzu-size;
        $zure: 16px;
        bottom: $zure;
        right: $zure;
        box-shadow: $zure $zure 12px hsla(56, 81%, 65%, 0.4);
    }
    &::after {
        background-color: hsla(85, 60%, 56%, 0.4);
        $size: 8px;
        width: $size;
        height: $size;
        top: 20px;
        left: 40%;
        box-shadow: 1px 1px 1px hsla(85, 40%, 16%, 0.2);
    }
}
  • top:100vhとしているのは、落下後の位置をベースにしているためです。完全に見えなくなるまで落とします
  • z-index: 9999は、そのページの最前面になるようにします

(1) 柚子を落とす

(0)で作った柚子を画面下部に落とします。
ベースの位置をtop:100vhとしているので、開始位置は-100vh・・・より少し下からでいいでしょう。
これは、もう単純にtranslateで↓に向けて移動させるだけです。

柚子が落ちるCSS
@keyframes yuzu-fall {
    0% {
        transform: translateY(-98vh);
    }
    100% {
        transform: translateY(0vh);
    }
}

パッと現れるのも味気ないので、すこしフェードインをかけてみることにします。

fadeinのCSS
@keyframes fade-in {
    0% {
        opacity: 0;
    }
    100% {
        opacity: 1;
    }
}

上記2つのアニメーションを.yuzuに設定します。

落ちる柚子のアニメーション部分のCSS
$fall-duration: 700;
$delay: 300;
.yuzu {
    animation-name: fade-in, yuzu-fall;
    animation-duration: 200ms, #{$fall-duration}ms;
    animation-timing-function: linear, cubic-bezier(.4,0,1,1);
    animation-iteration-count: 1, 1;
    animation-delay: #{$delay}ms, #{$delay}ms;
}

これで落ちます。ヒュンっと落ちます。

(2) 柚子を浮かべる

柚子湯を再現したいので、柚子を落としっぱなしにせずにぷかぷか浮かべたいと思います。
これも上下に延々動かせばいいので簡単ですが、落とすものと同じ.yuzuに対してアニメーションをかけるので、transformは使えません。そのかわりtopを使います。(逆でもいいです。むしろ逆のほうが楽かもしれません。)

ぷかぷかするアニメーションCSS
@keyframes yuzu-puka {
    0% {
        top: 100vh;
    }
    100% {
        top: calc(100vh - #{$yuzu-size/10 * 4});
    }
}

これを、(1)で書いた.yuzuに追記します。
animation-iteration-countはinfinite(ずっと繰り返す)。
animation-delayは落ちてから少し待って開始します。
animation-directionはalternate(順送り逆送りを交互に繰り返す)とします。

柚子が落ちてぷかぷかするまでのアニメーションCSS
$fall-duration: 700;
$delay: 300;
.yuzu {
    animation-name: fade-in, yuzu-fall, yuzu-puka;
    animation-duration: 200ms, #{$fall-duration}ms, 4s;
    animation-timing-function: linear, cubic-bezier(.4,0,1,1), cubic-bezier(0.55, 1.6, 0.57, -0.15);
    animation-iteration-count: 1, 1, infinite;
    animation-delay: #{$delay}ms, #{$delay}ms, #{$fall-duration + $delay + 300}ms;
    animation-direction: alternate;
}
  • animation-durationは4秒としていますがお好みでもう少しゆっくりでもいいです
  • animation-timing-functionもお好みで調整してみるといいと思います

これで、落ちて→ぷかぷかまでできました。

(3) 湯気を出す

柚子をぷかぷか浮かばせるだけでは、まだ柚子湯ではありません。
ほっこり感を出すために、湯気を作っていきます。

(3)-1 湯気のベースを作る

湯気感を出すために、要素の実体は見せずにbox-shadowを駆使していきます。
ぼわっという感じがある程度簡単に出せるのと、box-shadowならばコンテンツの邪魔をしないからです。

湯気要素
<div class="yuge"></div>
湯気要素の実体部分のCSS
.yuge {
    top: 100vh;
    position: fixed;
    background-color: transparent;
    border-radius: 50%;
}

そして、画面の下の方にたまる大きな湯気を作ります。

下の方にたまる湯気のベース
.yuge {
    width: 40vw;
    height: 10vh;
    opacity: 0.6;
    box-shadow: 20vw -30vh 0 hsl(0, 0%, 100%);
}

一旦、ぼかしを0にしてだいたいのポジションを決めてから・・・
湯気の素.png
ぼかしを設定します。

下の方にたまる湯気のベース(ぼかし)
.yuge {
    width: 40vw;
    height: 10vh;
    opacity: 0.6;
    box-shadow: 20vw -30vh 120px hsl(0, 0%, 100%);
}

湯気.png

(3)-2 湯気を大量生産する

ひとつだけだとイマイチ湯気っぽくないので、これをたくさん増やします。

CSSを使ってひとつの要素で描ける●(まる)の数は・・・?
で教えていただいたとおり、1つの要素にbox-shadowはいくらでもつけられるので、この仕様を利用します。

同じ場所にたくさんつくっても意味がないのである程度ランダムに配置するsassの関数を作り・・・

ランダム湯気
@function yuge($max-top-vh: 100) {
    @return #{random(50)}vw -#{random($max-top-vh)}vh #{160 + random(50)}px lighten(black, random(10) + 90%);
}

.yugeのbox-shadowに設定します。

大量の湯気
    box-shadow: yuge(60), yuge(60), yuge(60), yuge(60), yuge(60), yuge(60), yuge(60), yuge(60), yuge(60), yuge(60), 
                yuge(60), yuge(60), yuge(60), yuge(60), yuge(60), yuge(60), yuge(60), yuge(60), yuge(60), yuge(60),
                yuge(60), yuge(60), yuge(60), yuge(60), yuge(60), yuge(60), yuge(60), yuge(60), yuge(60), yuge(60);

こんな感じに、けっこう湯気っぽく見えてきます。
湯気大量.png

あとはこれを下からうえに向けてゆっくり持ち上げていくと・・・

湯気発生アニメーション
@keyframes bottom-yuge-up {
    0% {
        opacity: 0;
        transform: translateY(100vh);
    }
    99%, 100% {
        opacity: 0.6;
        transform: translateY(0vh);
    }
}
湯気発生アニメーション設定部文
.yuge {
    animation-name: bottom-yuge-up;
    animation-duration: 12s;
}

より湯気っぽくなります。

(3)-3 うごきのある湯気も作る

止まっているだけではほっこり感が足りません。
ほっこりさせるために、動きのある湯気も作りましょう。

静的湯気をそのままは使えないため、(3)-2までで作った湯気を「bottom-yuge」としてHTMLとCSSを作り変えます。

溜まってるだけの湯気要素
<div class="yuge bottom-yuge"></div>
湯気要素の実体部分のCSS
.yuge {
    position: fixed;
    background-color: transparent;
    border-radius: 50%;
}
.bottom-yuge {
    top: 100vh;
    width: 40vw;
    height: 10vh;
    opacity: 0.6;
    box-shadow: yuge(60), yuge(60), yuge(60), yuge(60), yuge(60), yuge(60), yuge(60), yuge(60), yuge(60), yuge(60), 
                yuge(60), yuge(60), yuge(60), yuge(60), yuge(60), yuge(60), yuge(60), yuge(60), yuge(60), yuge(60),
                yuge(60), yuge(60), yuge(60), yuge(60), yuge(60), yuge(60), yuge(60), yuge(60), yuge(60), yuge(60);
    animation-name: bottom-yuge-up;
    animation-duration: 12s;
}

そして、動く湯気の実体を作っていきます。
柚子の逆でゆーーーーーっくり下から上に動きます。

動く湯気要素
<ul class="hokkori-list">
  <li class="yuge hokkori-yuge"></li>
</ul>
動く湯気アニメーション(hokkori-yuge)
@keyframes hokkori-yuge {
    0% {
        opacity: 0;
        transform: translateY(0vh);
    }
    50% {
        opacity: 0.8;
        transform: translateY(-50vh);
    }
    99%, 100% {
        opacity: 0;
        transform: translateY(-100vh);
    }
}
動く湯気のスタイル
.hokkori-yuge {
    display: block;
    opacity: 0;
    animation-name: hokkori-yuge;
    animation-iteration-count: infinite;
    animation-timing-function: linear;
    box-shadow: yuge(100);
    $size: 120px;
    width: #{$size}px;
    height: #{$size * 1.3}px;
    top: #{100 + random(50)}vh;
    left: #{random(50)}vw;
    $delay: 5000 + random(3000);
    animation-delay: #{$delay}ms;
    animation-duration: #{5000 + random(10000)}ms;
}

位置やアニメーションの発動タイミング・長さなどを変えられるようにして、とにかくランダムに配置していくと、湯気感がどんどん増していきます。

最後に

画面一面を柚子湯に仕立て上げることはできましたが、アニメーションだの半透明だのを駆使するとめちゃくちゃ重くなります。
スマートフォンで見ていると手がほかほかしてくる程に負荷がかかります。

やりすぎ注意・増やしすぎ注意です。

サンプルソースを公開しました

参考資料