Humanity

Edit the world by your favorite way

CSS3 アニメーションで猿 (3)

過去記事はこちらからどうぞ。 トップの Vim アイコンをホバーして「Click Me!」をクリックでもいけます。

猿って何?

まず何のことを言ってるのか分からないと思うので、 過去記事を見てもらうか、この動画で概要を把握できます。


CSS3 アニメーションで猿

Vim のアイコンを1回クリックすると上に飛び立って、2回目クリックするとスゥーッと降りてくるというのを実現してみた、という事です。 今回はこれをどうやって実現したか、3回目にもなるのでちょっとだけちゃんと解説してみようと思います。

ソースコード

とりあえず HTML と CSS を冒頭に貼っておきます。

「デザイン設定 → ヘッダ → タイトル下」に以下の HTML を追加。

<div class='vim-icon-parent'>
  <details open='init'></details>
  <div class='zimbu'></div>
  <div class='vim-icon'></div>
  <div class="vim-icon-clickme"><a href='/search?q=CSS3+%E3%82%A2%E3%83%8B%E3%83%A1%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%81%A7%E7%8C%BF'>Click Me!</a></div>
</div>

「デザイン設定 → デザイン CSS」に以下の CSS を追加してます。 自分のはてなフォトライフに上げた画像を直リンクしてるので、 どうしても使いたい方は自分のフォトライフに上げるなりなんなりして下さい (気まぐれに画像消す可能性もあるので保証できない)。 まぁいないと思いますが。

/* 猿が下降する */
@keyframes saru-ga-modoru {
  0% {
    transform: translateY(-999px);
  }
}

/* 猿が上昇する */
@keyframes saru-ga-ugoku {
  0% {
    transform: translateY(0);
  }
  10% {
    transform: translateY(100px);
  }
  50% {
    transform: translateY(-300px);
  }
  60% {
    transform: translateY(-250px);
  }
  70% {
    transform: translateY(-300px);
  }
  80% {
    transform: translateY(-250px);
  }
  100% {
    transform: translateY(-999px);
  }
}

/* 透明にして .vim-icon と重ねる (.vim-icon と同じサイズ) */
.vim-icon-parent details {
  opacity: 0;
  position: absolute;
  left: 0;
  top: 0;
  width: 24px;
  height: 24px;
}
.vim-icon-parent .zimbu {
  will-change: transform;
}
/* 1. 初期表示 */
.vim-icon-parent details[open="init"] + .zimbu {
  animation: none;
}
/* 2. 上昇 */
.vim-icon-parent details:not([open]) + .zimbu {
  animation: saru-ga-ugoku 2.0s cubic-bezier(0,1,1,0) forwards;
}
/* 3. 下降 */
.vim-icon-parent details[open=""] + .zimbu {
  animation: saru-ga-modoru 2.0s ease-out forwards;
}

.vim-icon-parent {
  height: 30px;
  position: relative;
}
.vim-icon {
  display: inline-block;
  vertical-align: middle;
  width: 24px;
  height: 24px;
  background-image: url("//cdn-ak.f.st-hatena.com/images/fotolife/t/tyru/20160507/20160507085743.png");
}
.zimbu {
  width: 394px;
  height: 236px;
  background-image: url("//cdn-ak.f.st-hatena.com/images/fotolife/t/tyru/20160507/20160507093322.png");
}

.vim-icon-parent .zimbu {
  position: fixed;
  left: 0;
  bottom: 0;
  z-index: 99;
}
.vim-icon-parent:not(:hover) .zimbu {
  transform: translateY(240px);
}
.vim-icon-parent:hover .zimbu {
  transform: translateY(0);
}

.vim-icon-parent .vim-icon-clickme {
  position: absolute;
  left: 30px;
  display: inline-block;
  visibility: hidden;
}
.vim-icon-parent .vim-icon-clickme a {
  text-decoration: none;
}
.vim-icon-parent:hover .vim-icon-clickme {
  visibility: visible;
  border: 1px solid white;
  border-radius: 5px;
  padding: 5px 10px;
}

どうやって実現しているか

<details> 要素 + CSS アニメーションです。 前は <details> の代わりに <input type='checkbox'> を使ってたりしてたのですが、後述する理由でやめました。

前提知識

まず前提知識から。

<details> は例えばこういうやつです。

<details>
  <summary>サマリー</summary>
  本文
</details>

サマリー 本文

クリックしてみると開きます。 このように <details> は開閉します。 しかも開閉状態を DOM 上の open 属性で記録するため、CSS から属性セレクタで指定できるのです。 どういう事かというと、

閉じてる時

<details>
  <!-- 省略 -->
</details>

開いてる時

<details open>
  <!-- 省略 -->
</details>

<details open> のように開いてる時は open 属性が付くという事です。 属性が変わるという事は

details[open] {
  /* 開いてる時のスタイル */
}
details:not([open]) {
  /* 閉じてる時のスタイル */
}

のように CSS で指定できるという事です。 このようなタグは HTML と CSS だけで状態を持った動きを実現するのに使えます。 似たようなタグとして <input type='checkbox'> があり、 これも :checked, :not(:checked) を使えばそれぞれ指定できます。 つまりプログラムで言えば <details><input type='checkbox'> を使うのは boolean 変数1つ宣言するのと似たようなものです (=1bit の情報を保存できる)。

…とこれまで思ってたんですが、 <details> 要素に関しては 1bit ではなく3通りの状態を保存できました。 どういう事かというと、 <details open='init'></details>open 属性に何か空文字以外を指定しておくとクリックする度以下の様に変わります。

  1. <details open='init'>
  2. <details>
  3. <details open>

2 移行クリックするたび 2,3 の繰り返しです。 つまり初期表示の時だけ違う状態にする事ができます。 これが今回必要な要素でした。

これで CSS でそれぞれ指定できます。

details[open="init"] {
  /* 1. 初期表示 */
}
details:not([open]) {
  /* 2. 閉じてる時のスタイル */
}
details[open=""] {
  /* 3. 開いてる時のスタイル */
}

<input type='checkbox'> の indeterminate *1 を使って同じ事ができるかと思ったのですが、以下の理由からダメでした。

  • <input type='checkbox' indeterminate> と書けば indeterminate 状態に…できなかった
  • <details> 要素と違って DOM の属性が動的に変わらなかった

ソースコード解説

前提知識を解説した所で本編です。

まず <details>Vim のアイコンを重ねて、Vim のアイコンをクリックしたら <details> もクリックしたことにします。

<div class='vim-icon-parent'>
  <details open='init'></details>
  <!-- 省略 -->
  <div class='vim-icon'></div>
  <!-- 省略 -->
</div>
.vim-icon-parent {
  height: 30px;
  position: relative;
}
.vim-icon-parent .vim-icon {
  display: inline-block;
  vertical-align: middle;
  width: 24px;
  height: 24px;
  background-image: url("//cdn-ak.f.st-hatena.com/images/fotolife/t/tyru/20160507/20160507085743.png");
}
.vim-icon-parent details {
  opacity: 0;               /* 透明にして見えなくする (ただ opacity なのでクリック判定は残る) */
  position: absolute;  /* .vim-icon と重ねる */
  left: 0;
  top: 0;
  width: 24px;    /* .vim-icon と同じ幅 */
  height: 24px;   /* .vim-icon と同じ高さ */
}

あとは CSS アニメーションを適用するだけです。

  1. 初期表示 (<details open='init'>)
  2. 上昇する (<details>)
  3. 下降する (<details open>)
/* 1. 初期表示 */
.vim-icon-parent details[open="init"] + .zimbu {
  animation: none;
}
/* 2. 上昇 */
.vim-icon-parent details:not([open]) + .zimbu {
  animation: saru-ga-ugoku 2.0s cubic-bezier(0,1,1,0) forwards;
}
/* 3. 下降 */
.vim-icon-parent details[open=""] + .zimbu {
  animation: saru-ga-modoru 2.0s ease-out forwards;
}

上昇の時に saru-ga-ugoku, 下降の時に saru-ga-modoru を適用してそれぞれのアニメーションを実行します。 forwards (animation-fill-mode) は @yui540 さんの動画で知ったやつです。ありがとうございます。

www.youtube.com

おまけ:より効率的な CSS アニメーションを書く

.vim-icon-parent .zimbu {
  will-change: transform;
}

will-change プロパティで変わる可能性のあるプロパティを指定すると GPU 使ってくれて速くなったりするらしい。 正直体感はできてないですが… あと position: absoluteleft 使うより transform プロパティで移動した方が他の DOM 要素の位置を再計算する必要がない (ただその DOM 要素だけを移動・変形するだけ) ため速いとかなんとか。 詳しくは以下の記事とか、その他記事が沢山あるので検索して下さい。

感想

当初はちゃんとした記事を書くつもりはなかったですが、色々整理する意味でも書けて良かったと思います。

*1:専用の疑似要素あったんですね…知らなかった…