2020.02.06

コーディング

デザイン

見たことのないホバーアニメーションを作ろう!CSSanimation@keyframes

前置き

フロントエンドの葉っぱ一号です。

この度、GMOインターネットグループ宮崎採用サイトにこの宮崎クリエイターズブログのバナーを置かせてもらえることになり、デザイナーのayakaさんがこちらのバナーを作ってくれました!👇

宮崎クリエイターズブログ

バナー設置を担当することになった私は意気揚々と作業を終え、ayakaさんに表示確認をお願いします。

うどんたぬき

テストアップしました!
ご確認よろしくお願いします!

たかがバナーを設置するだけの簡単な作業。
ミスなどあろうはずもありません。

すみません……
マウスホバーしたときに
何かエフェクトをつけてもらってもいいですか?

……!

忘れてました。

うっかりを反省しつつ席に戻ると、

これ、ホバーエフェクトまだ付いてない?

フロントエンドの先輩であるMASATOSHIさんからも同じご指摘が!

うっかりを海より深く反省しつつ言葉を返そうとしたとき、

まだ付いてないんだったらさ、
なんかこう……「きらーん」って感じの、どう?

……………………!!

はい、そんな経緯で今回のタイトル👇

見たことのない
ホバーアニメーション
を作ろう!

     ・
     ・
     ・
少し省略しすぎたでしょうか。
もうちょっと詳しく説明してみようと思います。

ルールの設定

なんかたぬきが閃いた感じになってましたが、特に雷に打たれたというようなこともなく、ただただ次のようなことを考えたのでした。

・せっかくだから、この素敵なバナーにホバーアニメーションを付けよう!
・どうせなら、見たことないような面白い動きを作ってみよう!
・ブログのネタにもなるし!

ブログのネタにすることで心が決まった私は、どんなホバーアニメーションが面白いだろうか、と考えます。

  • MASATOSHIさんの提案である「きらーん」はもちろんのこと、ブログ記事にするとなるとあと2種類くらいはパターンがあった方がいいだろうなぁ。
  • いくら面白いと言っても、装飾用にhtml要素を追加しまくるのはいかがなものか……そこを許してしまうとキリがなくなりそうだし。
  • jsもなしでいこうかな……やれることが多すぎてキリがなくなりそうだし。
  • keyframesきちんと使ったことなかったし、この機会にいろいろ試してみるのもいいかな。

こんな感じで考えているうちに↓の3つのルールに収束していきました。

  1. CSSだけでできるものにする。jsは使わない。
  2. 実際にバナーを設置したときの構造から変更しない(装飾のために要素追加したりしない)
  3. 3つのテーマを考えて、それぞれに「はっちゃけ度:0%、50%、100%」の3パターンを考える。(合計9つ)

ルール2.の補足として、実際にバナー設置したときのhtml要素はこちらになります。👇

<a href="(ブログへのURL)">
    <img src="(バナー画像)" alt="(alt文)">
</a>

最低限という感じですね。

要素を装飾する際はみなさんbefore,after疑似要素を使われることが多いと思いますが、<img>は空要素なので疑似要素を持つことができません。(↓孫引きみたいになってしまいますが……)

CSS 2.1の仕様書にあたる。

As their names indicate, the :before and :after pseudo-elements specify the location of content before and after an element’s document tree content.
Generated content, automatic numbering, and lists

文書ツリー内容(Document tree content)を持たない要素(空要素)には、疑似要素内容は挿入されないようだ。
出典:rikubaのブログ|空要素に対するCSSの疑似要素(:before, :after)指定

<img>を<div>なんかでラッピングしていたり、<img>の代わりに<div>自身に画像を表示していたりすれば使える疑似要素が増えて装飾ももっとやりようはあったと思うんですが、実際こうだったのだから仕方がありません。

よって今回のホバーアニメーション作成に活用できるのは
<img>要素自身
<a>要素自身
<a>要素のbefore疑似要素
<a>要素のafter疑似要素
の4つになります。

今思えば今回のホバーアニメーション作成における最大のキモがこの限られた要素でどれだけ盛りだくさんに見せることができるかだったように思います。

また、他にも小さいルールとして「はっちゃけ度0%に関してはIEでも変わりなく動作するものにする」というのがありましたが、逆に言えば50%と100%ではIEは無視しました。

ですが実際作ってみたところ、今回作成したアニメーションの中でIEで再現できなかったのはfilterプロパティくらいでした。あとkeyframes中でのtransformの変化の動きがちょっと怪しいくらいでしょうか(動きはする)。

ただ個人的にはfilterプロパティが今回のホバーアニメーションの中でも一番面白い部分だったと思うので、やっぱりIEはもうバグるとかではない限り表示が違ってもOKくらいの付き合い方が良いと思います……。
IEの時は「Edgeに変えろ」とアラーム出しましょう。みんなで。

イメージを膨らませる

さて、ルールが定まると同時にルール3の「3つのテーマ」に関しても、ぼんやりとしてたイメージが出来上がって来ていました。

キーワードは
「鼓動」
「きらーん」
「ノイズ」

なぜこの3つが浮かんだのかというのは自分にも分かりませんが、もしかすると音までイメージできるものだったからかもしれません。
様々なイメージが浮かんでは消え、浮かんでは消えしていく中で、音を伴うイメージというのは頭に残りやすかったのでしょう。(予想)

イメージに音が付いているのですから、アニメーションの動きを作るときも「イメージした音が聞こえるような動きになっているか」を意識していた気がします。

たとえそれが無音のgifアニメだったとしても「なぜだか音が聞こえる気がする」という体験、皆さんもないですか?

『生き生きとしたアニメーションとは、音が連想されるものである』

今適当に考えました。

でもあながち間違っていない気がします。

次から、テーマごとに「はっちゃけ度:0%、50%、100%」のホバーアニメーションをつけたバナー画像が続きます。
9つの内どれか一つからでも音が聞こえる気がしてもらえたら幸いです。

やっと始まるだなも

鼓動

最初のテーマは鼓動です。
Heartbeatです。
分かりやすい動きなので一番初めに作り始めました。

スマホユーザー用にホバーON/OFFスイッチを配置してます。(タップすればホバー判定発生するみたいですが念のため)
また、コメント解説的なものも入れてみました。
興味ある方はクリックしてみてください。

鼓動 はっちゃけ度:0%

解説

普通ですね。
解説というほどのこともないのでCSSだけ載せておきます。

@keyframes heartbeat {
  0% {
    transform: scale(1);
  }
  4% {
    transform: scale(1.02);
  }
  8% {
    transform: scale(1);
  }
  12% {
    transform: scale(1.02);
  }
  16% {
    transform: scale(1);
  }
}
a:hover {
  opacity: 0.8;
  animation-duration: 2s;
  animation-iteration-count: infinite;
  animation-name: heartbeat;
  animation-timing-function: linear;
}

はっちゃけ度0%は基本的にこんなもんです。

close

鼓動 はっちゃけ度:50%

解説

はっちゃけ度0%の動きに
・明るさ
・広がる影
が追加された形です。

なんか心臓が脈打って血液ジワ~って感じです。

@keyframes heartbeat {
  0% {
    transform: scale(1);
  }
  4% {
    transform: scale(1.02);
  }
  8% {
    transform: scale(1);
  }
  12% {
    transform: scale(1.02);
  }
  16% {
    transform: scale(1);
  }
}
a:hover {
  opacity: 0.8;
  animation-duration: 2s;
  animation-iteration-count: infinite;
  animation-name: heartbeat;
  animation-timing-function: linear;
}
@keyframes brightness {
  10% {
    filter: brightness(1.5);
  }
  100% {

    filter: brightness(1);
  }
}
@keyframes shadow {
  0% {
    box-shadow: 0 0 0 0 rgba(0, 91, 172, 0.6);
  }
  100% {
    box-shadow: 0 0 50px 50px rgba(0, 91, 172, 0);
  }
}
a:hover img {
  animation-duration: 2s;
  animation-iteration-count: infinite, infinite;
  animation-name: brightness, shadow;
  animation-timing-function: linear, linear;
}

カブトガニの血は青いんですって

close

鼓動 はっちゃけ度:100%

解説

なんか色々オーバーにしてみました。

表示範囲に収まってない? 描画が怪しい?
そんなの関係ねぇ!!
これがはっちゃけ度100%のスタンスです。

とりあえずコードはこちら👇

a::before , a::after {
  content: "";
  background-image: url("本体と同じ画像");
  background-size: contain;
  vertical-align: middle;
  position: absolute;
  left: 0;
  top: 0;
  opacity: 0;
  display: none;
  width: 100%;
  height: 100%;
  transform: translate(-50%, -50%);
}
@keyframes heartbeat-max {
  0% {
    transform: scale(0.8);
  }
  4% {
    transform: scale(1.1);
  }
  8% {
    transform: scale(1);
  }
  12% {
    transform: scale(1.1);
  }
  16% {
    transform: scale(1);
  }
  100% {
    transform: scale(0.8);
  }
}
a:hover {
  animation-duration: 3s;
  animation-iteration-count: infinite;
  animation-name: heartbeat-max;
  animation-timing-function: linear;
}
@keyframes filter_max {
  0% {
    filter: saturate(5) blur(0) brightness(2);
  }
  50% {
    filter: saturate(3) blur(0) brightness(2);
  }
  100% {
    filter: saturate(0) blur(10px) brightness(1);
  }
}
a:hover img {
  animation-duration: 3s;
  animation-iteration-count: infinite;
  animation-name: filter_max;
  animation-timing-function: linear;
}
@keyframes afterimage {
  0% {
    transform: scale(1);
    opacity: 1;
    filter: saturate(10) brightness(2) blur(0);
  }
  40% {
    opacity: 0;
  }
  100% {
    transform: scale(2);
    opacity: 0;
    filter: saturate(5) brightness(1) blur(16px);
  }
}
a:hover::before,a:hover::after {
  display: inline-block;
  animation-duration: 3s;
  animation-iteration-count: infinite;
  animation-name: afterimage;
  animation-timing-function: cubic-bezier(0, 0.4, 0.4, 0.64);
}
a:hover::after {
  animation-delay: 0.24s;
}

いきなり結構長くなりましたが、ここにきてようやく解説っぽいの書きます。

<a>のbefore,after疑似要素に本体と同じ画像を設定し、脈打つタイミングに合わせて残像のように広げています。

animation-timing-functionにcubic-bezierを指定しているところがあります。
cubic-bezierの引数は自分でいい感じに調整しましょう。ブラウザで検索すればグラフィカルなツールも出てきますし、chromeならばデベロッパーツール画面上でもグラフをいじることができます。
アニメーションのイージングには様々なものがありますが、適当にデフォルトのものを選ばずに自分でいろいろと試してみると一気に動きが生き生きしてくることもありますよ!

また、今回はfilterプロパティに複数の指定をしているところがあります。

filter: saturate(5) blur(0) brightness(2);  //←こんな風に

こんな時の注意点としてkeyframesの中でfilterの中身を変化をさせようとしたとき、特定のfilter関数を変化させたくない区間があったとしても、省略して書くことはできません。

よく分からないと思うので例を出します。

@keyframes example {
  0% {
    filter: saturate(2) brightness(2) blur(3px);
  }
  50% {
    filter: saturate(5) brightness(2) blur(3px);
  }
  100% {
    filter: saturate(1) brightness(2) blur(3px);
  }
}

上記のようなkeyframeがあったとして、最後まで変化のないbrightnessやblurは書かなくていいんじゃないかなーと、

@keyframes example {
  0% {
    filter: saturate(2) brightness(2) blur(3px);
  }
  50% {
    filter: saturate(5);
  }
  100% {
    filter: saturate(1);
  }
}

↑このように省略してしまってはいけないという話です。
filterはfilter自体が一つのプロパティであり、filterで指定できる
・blur()
・brightness()
・contrast()
・drop-shadow()
・grayscale()
・hue-rotate()
・invert()
・opacity()
・saturate()
・sepia()
これらはプロパティではなく関数です。
filterプロパティに書かれていて初めて動作するもので、個別に保持されているものではないようです。
よって、

@keyframes example {
  0% {
    filter: saturate(2) brightness(2) blur(3px);
  }
  50% {
    filter: saturate(5);
  }
  100% {
    filter: saturate(1);
  }
}

↑このように書いてしまうと、0%である一瞬だけbrightnessとblurが発動し、次の瞬間に適応されるfilterプロパティにはbrightnessとblurの存在がなくなり、saturateのみの動作に切り替わってしまいます。
長いkeyframeを書くときなんかは結構煩わしいです。
コード的な見通しが悪いというだけではなく、animation-timing-functionがliner以外の時なんかは思ったような変化をさせずらいときがたくさんありました。

そんな訳で、この後のテーマでもfilterプロパティに関しては一見助長な記述に思える部分が出てきますがご了承ください。

個別にプロパティとして設定できればなぁ……

close

きらーん

お次はきらーんです。

はっちゃけ度0%のやつはよく見かけますね。
50%のやつは言うほどはっちゃけてません。が、100%はかなりはっちゃけたと思います。
はっちゃけすぎてfirefoxでは描写しきれていないエフェクトもあるようです。単体では問題ない描写も重なったり動いたりすることで描写が追い付かなくなったりするのでしょうか。ここはちゃんと調べ切れていません……
chromeでは問題ないのでchrome推奨です。

きらーん はっちゃけ度:0%

解説

よく見るタイプのやつです。
<a>のbefore,after疑似要素を半透明白に塗りつぶし、変形させてスライド移動させてます。
before,afterともに基本的に同じ設定をしてます。
同じ設定をした後にafterだけさらにheight(-45度回転させてるので感覚的には幅です)とアニメーション開始タイミング(animation-delay)を変更してます。

a {
  overflow: hidden;
}
a::before, a::after {
  background: rgba(255, 255, 255, 0.5);
  position: absolute;
  content: "";
  display: inline-block;
  transform: rotate(-45deg);
  width: 100%;
  height: 10%;
  left: -480px;
  bottom: 0;
}
a::after {
  height: 100%;
}
a:hover {
  opacity: 0.8;
}
@keyframes move {
  0% {
    left: -480px;
    opacity: 0.3;
  }
  10% {
    opacity: 0.8;
  }
  20% {
    left: 480px;
    opacity: 0.3;
  }
  100% {
    left: 480px;
    opacity: 0.3;
  }
}
a:hover::before, a:hover::after {
  animation-duration: 2.4s;
  animation-iteration-count: infinite;
  animation-name: move;
  animation-timing-function: cubic-bezier(0.5, 0, 0.5, 1);
}
a:hover::after {
  animation-delay: 0.05s;
}

これならkeyframesよりtransitionの方が良さそう……

close

きらーん はっちゃけ度:50%

解説

鼓動の時もそうでしたが、はっちゃけ度50%は言い過ぎですね。

前回は横にスライドするだけでしたが、今回は少しだけ動きに変化をつけています。
transformを使って回転の動き(Z軸回り)を追加し、perspectiveを変化させることで光自体の形も変化するようにしています。(早すぎて形の変化はあんまりわからないかもしれませんが……)
光の動きがよくわからない方はデベロッパーツール等で<a>に付いてるoverflowをvisibleにして、背景を暗めに設定するとどんな動きをしているのか分かりやすいと思います。

個人的にコツっぽいかなぁと思うのはtransform-originをちゃんと設定するところでしょうか。
transform-originはtransformの軸を設定するプロパティです。
transform-originを設定するのとしないのとではイメージ通りの動きに近づけるための楽さが違います。軸を対象要素の外側に設定することもできるのでかなり使いようがありますね。

おまけ程度にbox-shadowをinsetで付けています。
insetの影を画像の上に表示するためには<img>のpositionとz-indexをいじる必要があるみたいです。

a {
  overflow: hidden;
}
@keyframes move-second {
  0% {
    left: -240px;
    opacity: 0.3;
    transform: perspective(1px) rotateZ(0deg) rotateY(3deg);
  }
  20% {
    opacity: 1;
  }
  40% {
    left: 20px;
    opacity: 0.3;
    transform: perspective(100px) rotateZ(180deg) rotateY(3deg);
  }
  100% {
    left: 20px;
    transform: perspective(100px) rotateZ(180deg) rotateY(3deg);
    opacity: 0.3;
  }
}
a::before, a::after {
  background: linear-gradient(0deg, transparent 0, rgba(255, 253, 221, 0.8) 35%, rgba(255, 255, 255, 0.9) 50%, rgba(255, 253, 221, 0.8) 65%, transparent 100%);
  position: absolute;
  content: "";
  display: inline-block;
  width: 100%;
  height: 50%;
  left: 0;
  bottom: -100%;
  transform-origin: 100% 0;
}
a:hover {
  box-shadow: inset 0 0 10px 5px #fffddd;
}
a:hover img {
  position: relative;
  z-index: -1;
}
a:hover::before, a:hover::after {
  display: inline-block;
  animation-duration: 2.4s;
  animation-iteration-count: infinite;
  animation-name: move-secound;
  animation-timing-function: cubic-bezier(0.5, 0, 0.5, 1);
}
a:hover::after {
  animation-delay: 0.1s;
}

きらーんって感じではないよなぁ……

close

きらーん はっちゃけ度:100%

解説

どうですか! ふざけてるでしょう!
50%が地味だったので、100%ははっちゃけなきゃ……って感じで作りました。

イメージはなんかアクションゲームとかで1ステージに1枚だけ隠してあるでっかいコイン的なやつです。
コインといえば浮いてるので浮かせました。

とりあえずコード👇
長いです。

a {
  overflow: hidden;
  transition: transform 0.3s;
  box-sizing: border-box;
  position: relative;
}
a::before {
  background: linear-gradient(0deg, #fffddd 0%, rgba(255, 253, 221, 0.6) 10%, rgba(255, 253, 221, 0.1) 20%, rgba(255, 253, 221, 0.8) 35%, white 50%, rgba(255, 253, 221, 0.8) 65%, rgba(255, 253, 221, 0.1) 75%, rgba(255, 253, 221, 0.6) 85%, #fffddd 100%);
  position: absolute;
  content: "";
  display: inline-block;
  width: 100%;
  height: 100%;
  left: 0;
  bottom: -200%;
  transform: perspective(1px) rotateZ(0deg) rotateY(0deg);
}
a::after {
  content: "";
  width: 40px;
  height: 40px;
  background-image: radial-gradient(circle at 0% 100%, transparent 20px, #fff 21px), radial-gradient(circle at 100% 100%, transparent 20px, #fff 21px), radial-gradient(circle at 100% 0px, transparent 20px, #fff 21px), radial-gradient(circle at 0px 0px, transparent 20px, #fff 21px);
  background-position: bottom left, bottom right, top right, top left;
  background-size: 50% 50%;
  background-repeat: no-repeat;
  transform-origin: 50% 50%;
  position: absolute;
  top: 0;
  left: 0;
  display: none;
}
@keyframes shine {
  0% {
    filter: sepia(85%) brightness(1.4);
  }
  100% {
    filter: sepia(90%) brightness(1.8);
  }
}
@keyframes floating {
  0% {
    top: -5px;
  }
  100% {
    top: 5px;
  }
}
@keyframes floating-shadow {
  0% {
    box-shadow: 0px 110px 40px -65px rgba(0, 0, 0, 0.5);
  }
  100% {
    box-shadow: 0px 80px 40px -40px rgba(0, 0, 0, 0.5);
  }
}
a:hover {
  animation: 0.6s 0s alternate infinite none cubic-bezier(0.5, 0, 0.5, 1) shine, 0.8s 0.3s alternate infinite both cubic-bezier(0.5, 0, 0.5, 1) floating, 0.8s 0.3s alternate infinite both cubic-bezier(0.5, 0, 0.5, 1) floating-shadow;
  transform: translateY(-20px);
  border-top: 1px solid #807d64;
  border-left: 1px solid #807d64;
  border-right: 1px solid #6b6952;
  border-bottom: 1px solid #6b6952;
  outline: outset 12px rgba(128, 125, 100, 0.5);
  outline-offset: -12px;
}
@keyframes move-max {
  0% {
    left: -600px;
    bottom: -250px;
    opacity: 0.3;
    transform: perspective(1px) rotateZ(-90deg) rotateY(0deg);
    transform-origin: 100% 100%;
  }
  40% {
    opacity: 1;
    bottom: -150px;
  }
  80% {
    left: 100px;
    opacity: 0.3;
    bottom: -300px;
    transform: perspective(100px) rotateZ(120deg) rotateY(4deg);
    transform-origin: 100% 50%;
  }
  100% {
    left: 100px;
    bottom: -300px;
    transform: perspective(100px) rotateZ(120deg) rotateY(4deg);
    opacity: 0.3;
    transform-origin: 100% 50%;
  }
}
a:hover::before {
  animation-duration: 2s;
  animation-iteration-count: infinite;
  animation-name: move-max;
  animation-timing-function: cubic-bezier(0.5, 0, 0.5, 1);
}
@keyframes star {
  0% {
    transform: scale(0);
  }
  39% {
    transform: scale(1) scaleY(2);
  }
  39.1% {
    transform: scale(0);
  }
  40% {
    transform: scale(0);
  }
  99% {
    transform: scale(1.2) scaleY(2);
  }
  99.1% {
    transform: scale(0);
  }
  100% {
    transform: scale(0);
  }
}
@keyframes star_move {
  0% {
    top: 20%;
    left: 2%;
  }
  20% {
    top: 5%;
    left: 4%;
    opacity: 1;
  }
  20.1% {
    opacity: 0;
  }
  20.9% {
    opacity: 0;
  }
  21% {
    opacity: 1;
    top: 57%;
    left: 89%;
  }
  30% {
    top: 56%;
    left: 91%;
  }
  49% {
    top: 45%;
    left: 92%;
    opacity: 1;
  }
  49.1% {
    opacity: 0;
  }
  49.9% {
    opacity: 0;
  }
  50% {
    opacity: 1;
    top: 45%;
    left: 12%;
  }
  70% {
    top: 56%;
    left: 14%;
    opacity: 1;
  }
  70.1% {
    opacity: 0;
  }
  70.9% {
    opacity: 0;
  }
  71% {
    opacity: 1;
    top: 13%;
    left: 80%;
  }
  80% {
    top: 13%;
    left: 83%;
  }
  100% {
    top: 22%;
    left: 86%;
  }
}
a:hover::after {
  animation-name: star, star_move;
  animation-duration: 0.5s, 2s;
  animation-iteration-count: infinite;
  animation-timing-function: cubic-bezier(0.5, 0, 0.5, 1);
  display: inline-block;
}

盛り込めるだけ盛り込んだ感じですね。
使えるヤツは四人しかいないので(<img>、<a>、<a>のbefore、<a>のafter)、本来なら数人にやらせたいような仕事を一人に押し付けたようなコードです。

それぞれのやっていることを追ってみます。

・<a>
<a>は4つのことをやってもらっています。
animationの記述がかなり長くなったのでここだけショートハンドでの記述です。

(1)ピカピカしてもらいます。
keyframes「shine」の中でfilterプロパティを変化させています。使用している関数はsepia()とbrightness()で、sepia()は本来その名の通り画像をセピア色にしてなんかいい感じの古さを演出するものだと思いますが、見ようによっちゃゴールドっぽくね? ということで採用しました。というかこのゴールドっぽくね? という気づきで「きらーん:はっちゃけ度100%」の方向性は決まったようなものです。
sepia()とbrightness()を軽く変化させることでピカピカ感を出そうとした感じですね。

(2)ホバー後フワフワと浮いてもらいます。
動きとしてはまず「transform: translateY」で一定の高さまで浮き上がり、その動きが終わるのを待って(animation-delay)、keyframes「floating」でtopの値を振り子のように変化させています。
位置を変更させる手段としてtransformプロパティとtopプロパティ二つを使用している形です。
transitionとkeyframesで同じプロパティを操作しようとするとバッティングしてうまくいかなかったためこのような形になりました。

(3)影も担当してもらいます。
当初は影は形が自由に設定できる疑似要素で作成するつもりでした。しかし、疑似要素にはもっと大事なお仕事があったのです(後述)。
仕方ないのでなんとかそれっぽく見えるようにbox-shadowの形を小さくしました。
残念だったのが、マウスホバー時の最初の浮き上がるときの動きをbox-shadowで出せなかったことです。transitionとkeyframesで同じプロパティを設定すると前記したほうが後記したほうに上書きされてしまいました。animation-delayでtransitionの動きが終わるのを待ってもダメでした。(自分ができなかっただけで両立できるんでしょうか? 知っている人いたら教えてほしいです)
仕方がないので、浮き上がり時の影の動きは最初の透明度を低くすることで違和感をできるだけ軽減させ、フワフワの時の影の大きさ変化を優先させることにしたのでした。

(4)立体感演出のために線を付けてもらいます。
バナーの形やsepia()での色付けの結果、「もうこれは金の延べ棒だろう」となりました。
金の延べ棒といえばあの形ですよね。なんていうんでしょうあの形。台形柱?
まぁとにかく立体です。平面ではありません。
最初はborderを半透明にして内側にセットできないかなぁと思っておりましたが、ふさわしいプロパティがいました。
outlineです。
borderの存在によってついつい忘れがち(個人的に)なoutlineですが、ちゃんと調べてみるとborderよりも使い勝手の良い場面もたくさんありそうです。
outlineにはそもそも浮き上がって立体に見せるという打ってつけの機能(outset)があったので即採用しました。
ただもったいないのでborderもつけてます。outlineよりも外側に1pxだけ明るめの線を引いてます。金属的な強めの照り返し感を出したかったのですが……うまくいってるかは微妙ですね。

・<a>のbefore疑似要素
<a>のbefore疑似要素には動くハイライト的な光を担当してもらいました。
はっちゃけ度0%と50%のときはbefore,after両方を使ってこの光を表現していましたが、疑似要素にはどうしてももう一つやってほしいことがあったので、今回はbefore疑似要素一人でなんとか表現できないかと考えました。
結果、backgroundにlinear-gradientを設定することで対応しました。このbackgroundにlinear-gradientを設定する手法は他にも色々な使い道があると思います。カラーも何色でも使えるので、レインボーな線をたくさん引いて、transformで変形すればライブ会場で飛び交うカラフルレーザー光線みたいなのもできそう。
このテーマの最初にも触れましたが、この光がfirefoxではうまく表示されませんでした。transformの値を小さくして動きを優しめに設定すると表示されるし、値によっては「頑張って表示しようとしてるけどキツイです」と言わんばかりに点滅しながら表示されるのでブラウザのグラフィック周りの限界なのでしょうか……?

・<a>のafter疑似要素
キラキラ担当。
どうしてもキラキラを入れたかったので頑張りました。
まずキラキラの形を作るのにもコツが要ります。背景を四分割してそれぞれに円形グラデーションをかけて形を作ります。
そしてkeyframes「star」でのサイズの変更。小さい→大きいへ。
さらにkeyframes「star_move」でのキラキラ自体の移動。
このサイズ変更と移動を調整するのに苦労しました。早すぎても遅すぎてもダメ。
最終的になんとかそれっぽくできたかな、とは思ってますがどうでしょう?
理想はキラキラがafter疑似要素一人だと気づかれないことです。自然と複数のキラキラがあるように見えてくれていたら成功です。
でも本当はafter疑似要素がひとりめっちゃ慌ただしく瞬いたり動き回ったりしてるのです。
そう思って改めて見てみると可愛くみえてこないですか? 私だけでしょうか。

・<img>
仕事のできる人に仕事が集まり、そんな忙しそうな人たちを横目に見ながら今日も今日とて暇を持て余す。<img>です。
仕事のできる疑似要素さんに頼みたい仕事はまだまだあったのですが<img>に頼みたい仕事はありませんでした。

よくあることですよね。

close

ノイズ

最後はノイズです。

一番時間がかかったテーマです。(はっちゃけ度100%のやつ)
最初浮かんでいたイメージはグリッチノイズでした。
試しに「グリッチノイズ css」なんかでググってみても出てくるのはjsやAfterEffectを使用したものばかり。

なんとかcssのみで近づけることができないかと試行錯誤したものが以下になります。

苦労した分はっちゃけ度100%のやつは一番お気に入りです。

ノイズ はっちゃけ度:0%

解説

今までのテーマと違って、これははっちゃけ度0%と言っていいのか微妙ですね。

いきなり<a>のbefore,after疑似要素両方に本体と同じ画像つっこんでます。
合計で三つ同じ画像があるわけですが、それぞれを半透明にしてbefore疑似要素は右上へ、after疑似要素は右下へと小刻みに動かしてみました。

ノイズっぽく見えるでしょうか?

a {
  overflow: visible;
}
a::before,a::after {
  content: "";
  background-image: url("本体と同じ画像");
  background-size: contain;
  vertical-align: middle;
  position: absolute;
  left: 0;
  top: 0;
  display: none;
  width: 100%;
  height: 100%;
}
@keyframes translateBefore {
  0% {
    top: 3px;
    left: -5px;
  }
  100% {
    top: -3px;
    left: 5px;
  }
}
@keyframes translateAfter {
  0% {
    top: -3px;
    left: -5px;
  }
  100% {
    top: 3px;
    left: 5px;
  }
}
a:hover img {
  opacity: 0.3;
}
a:hover::before {
  animation-name: translateBefore;
  animation-duration: 0.61s;
  animation-iteration-count: infinite;
  animation-timing-function: linear;
  opacity: 0.3;
  display: inline-block;
}
a:hover::after {
  animation-name: translateAfter;
  animation-duration: 0.79s;
  animation-iteration-count: infinite;
  animation-timing-function: linear;
  opacity: 0.3;
  display: inline-block;
}

before疑似要素とafter疑似要素のアニメーション周期(animation-duration)がそれぞれ0.61s、0.79sと中途半端な数字なのはなるべく周期が同期してほしくなかったからです。
なんとなくバラバラになりそうな感じの値にしました。

『ランダムっぽく』

これが今回のテーマ「ノイズ」の中で最も気を付けたテーマのなかのテーマなのです。

CSSでランダムが使えたらなぁ……

close

ノイズ はっちゃけ度:50%

解説

0%の動きにプラスして、三つの画像それぞれにfilterをかけたものがこちらです。
イメージ的には3D眼鏡で見る映像を裸眼で見た感じです。
CMYでやって印刷ズレた感出したかったんですが、使える疑似要素が2つなので妥協です。

a {
  overflow: visible;
}
a::before,a::after {
  content: "";
  background-image: url("本体と同じ画像");
  background-size: contain;
  vertical-align: middle;
  position: absolute;
  left: 0;
  top: 0;
  display: none;
  width: 100%;
  height: 100%;
}
a::before {
  filter: sepia(100%) hue-rotate(140deg) saturate(2);
  mix-blend-mode: darken;
}
a::after {
  filter: sepia(100%) hue-rotate(-100deg) saturate(2);
  mix-blend-mode: darken;
}
a:hover img {
  opacity: 0.4;
  filter: saturate(10);
}
@keyframes translateBefore {
  0% {
    top: 3px;
    left: -5px;
  }
  100% {
    top: -3px;
    left: 5px;
  }
}
@keyframes translateAfter {
  0% {
    top: -3px;
    left: -5px;
  }
  100% {
    top: 3px;
    left: 5px;
  }
}
a:hover::before {
  animation-name: translateBefore;
  animation-duration: 0.61s;
  animation-delay: 0;
  animation-iteration-count: infinite;
  animation-timing-function: linear;
  display: inline-block;
  opacity: 0.4;
}
a:hover::after {
  animation-name: translateAfter;
  animation-duration: 0.79s;
  animation-delay: 0;
  animation-iteration-count: infinite;
  animation-timing-function: linear;
  display: inline-block;
  opacity: 0.4;
}

before,after疑似要素にそれぞれsepia()を100%でかけてhue-rotate()しています。
単色(セピア)にした後に、色相回転させて赤単色と青単色にしてみたって感じです。
filterでRGBチャンネル個別抜き出しとかできれば、本体画像も合わせてRGBが揃いもっと面白くできたような気がするんですがそれは贅沢ですね。

mix-blend-modeは色々悩んだ結果darkenにしました。本体画像からズレた赤と青が一番見える気がしたので。

photoshopでは覆い焼きが好きです。

close

ノイズ はっちゃけ度:100%

解説

いよいよオオトリ。
一番のお気に入りです。
そして恐らく今までで一番長いコードをどうぞ👇

a {
  background: repeating-linear-gradient(0deg, #333 0, #333 1px, transparent 2px, transparent 4px, #333 5px);
}
a::before {
  filter: sepia(100%) hue-rotate(300deg) opacity(0.8);
  mix-blend-mode: hard-light;
}
a::after {
  filter: sepia(100%) hue-rotate(60deg) opacity(0.8);
  mix-blend-mode: hard-light;
}
@keyframes main_transform {
  0% {
    transform: scaleX(1) scaleY(1.1) skewX(60deg);
  }
  1% {
    transform: scaleX(1.02) scaleY(1);
  }
  2% {
    transform: scaleX(1);
  }
  20% {
    transform: skewX(0);
  }
  21% {
    transform: skewX(60deg);
  }
  22% {
    transform: skewX(-10deg);
  }
  24% {
    transform: skewX(0);
  }
  30% {
    transform: skewX(0);
  }
  31% {
    transform: skewX(120deg);
  }
  32% {
    transform: skewX(-10deg);
  }
  40% {
    transform: scaleX(0.95) scaleY(1);
  }
  41% {
    transform: scaleX(1.02) scaleY(0.9);
  }
  42% {
    transform: scaleX(1) scaleY(1);
  }
  44% {
    transform: scaleY(1);
  }
  45% {
    transform: scaleY(1.2);
  }
  46% {
    transform: scaleX(1) scaleY(1);
  }
  47% {
    transform: scaleX(1.02);
  }
  48% {
    transform: scaleX(1) rotateX(6deg);
  }
  50% {
    transform: skewX(0deg);
  }
  80% {
    transform: scaleX(1) rotateX(0);
  }
  85% {
    transform: scaleY(1) rotateX(-20deg);
  }
  86% {
    transform: scaleY(0);
  }
  87% {
    transform: scaleY(1);
  }
  90% {
    transform: scaleX(1.1) rotateX(0) skewX(-100deg);
  }
  91% {
    transform: scaleX(1) skewX(60deg);
  }
  92% {
    transform: scaleX(1) skewX(-160deg);
  }
  93% {
    transform: scaleX(1) skewX(100deg);
  }
  94% {
    transform: scaleX(1) skewX(-80deg);
  }
  94% {
    transform: skewX(0);
  }
  100% {
    transform: scaleX(1.01) skewX(0);
  }
}
@keyframes main_filter_max {
  0% {
    filter: saturate(1) contrast(1);
  }
  20% {
    filter: saturate(15) contrast(1.2);
  }
  22% {
    filter: saturate(0.5) contrast(1);
  }
  25% {
    filter: saturate(10) contrast(0.8);
  }
  30% {
    filter: saturate(5) contrast(1);
  }
  40% {
    filter: saturate(0.5) contrast(1.1);
  }
  50% {
    filter: saturate(5) contrast(5);
  }
  51% {
    filter: saturate(2) contrast(1);
  }
  60% {
    filter: saturate(10) contrast(3);
  }
  61% {
    filter: saturate(3) contrast(0.5);
  }
  70% {
    filter: saturate(1) contrast(2);
  }
  75% {
    filter: saturate(10) contrast(1);
  }
  80% {
    filter: saturate(2) contrast(2);
  }
  85% {
    filter: saturate(200) contrast(1);
  }
  90% {
    filter: saturate(10) contrast(1);
  }
  100% {
    filter: saturate(10) contrast(3);
  }
}
@keyframes main-bg {
  0% {
    background-position: 0px 0px;
  }
  100% {
    background-position: 0px 25px;
  }
}
a:hover {
  animation-name: main_filter_max, main_transform, main-bg;
  animation-duration: 10s, 4s, 3s;
  animation-iteration-count: infinite;
  animation-timing-function: ease-in-out, linear, linear;
  animation-direction: normal, alternate, normal;
}
a img {
  opacity: 0.5;
}
@keyframes opacityChange {
  0% {
    opacity: 0.3;
  }
  33% {
    opacity: 0.6;
  }
  66% {
    opacity: 0;
  }
  100% {
    opacity: 0.4;
  }
}
@keyframes zindexChange {
  0% {
    z-index: 1;
  }
  100% {
    z-index: -8;
  }
}
@keyframes translateBefore-max {
  0% {
    top: 4px;
    left: -5px;
  }
  100% {
    top: -4px;
    left: 5px;
  }
}
a:hover::before {
  animation-name: translateBefore-max, opacityChange, zindexChange;
  animation-duration: 0.61s, 3.1s, 4.7s;
  animation-iteration-count: infinite;
  animation-timing-function: linear, ease-in-out, steps(10);
  display: inline-block;
}
@keyframes translateAfter-max {
  0% {
    top: -4px;
    left: -5px;
  }
  100% {
    top: 4px;
    left: 5px;
  }
}
a:hover::after {
  animation-name: translateAfter-max, opacityChange, zindexChange;
  animation-duration: 0.79s, 2.8s, 3.4s;
  animation-iteration-count: infinite;
  animation-timing-function: linear, ease-in-out, steps(10);
  display: inline-block;
}

100%で新しく追加されたことといえば、<a>のbackgroundにrepeating-linear-gradientでシマシマを入れそれをbackground-positionで下方向へと移動させることでなんかブラウン管っぽい感じを出していることと、
<a>自体をなんか電波障害っぽく見えるように変形させまくってることとか、疑似要素のz-indexを一定時間ごとに切り替えていることでしょうか。

ついでにbefore,after疑似要素の画像の移動をほんの少し上げていたり、filterのかかり具合を強力にしたりもしています。

ここまできたら後はもう、いかにいい感じにランダムっぽく見えるようにketframesの中身を構築できるかの勝負です。

分けられるものは分け、それぞれに他とかぶり辛そうなanimation-durationを設定します。
もっとこんな感じに動いてほしい、もっとこんな感じに動いてほしいを追求した結果がこの長ったらしいkeyframesの数々です。
もっとシンプルでかつランダムに見えるkeyframesにもできたのでしょうが、時間的な意味でやめておきました。

周期をバラバラにした結果ずっと眺めていても少しづつ違った表示になり楽しいです。
よかったらしばらく眺めてやってください。

彩度がブワッとあがる瞬間が好きです。

close

まとめ と おまけ

今回初めてきちんとCSS Animationと向き合ってみて感じたことは、filterの面白さbox-shadowのシャドウ以外での汎用性疑似要素のなんでもできる感。それからtransformのrotate系とperspectiveの可能性です。

filterのおかげでcssのみで画像を加工できますし(簡単な加工はもうphotoshop要らずですね)、アニメーションにしちゃえば軽く動画チックな雰囲気も醸し出せると思います。

また、box-shadowの自由度がかなり高いことを今回知ることができました。
サイズも変えられるし何個でも使えるので影以外の装飾にもうまく使えると思います。

疑似要素に関してはもうなんでもありですね。サイズも形もグラデーションもtransformも画像も使えて、使える疑似要素の数が戦況を左右するって感じです。
今回の三つのテーマの中でも八面六臂の働きをしてくれました。やまとなでしこ七変化です。

そしてなにより、rotate系とperspectiveの組み合わせについては立体的な表現に対してかなり威力を発揮できそうだと感じました。
なんとなく考えただけでも、
👇こんなのとか

👇こんなのとか

perspectiveはその名の通り遠近感に関係するプロパティで、オブジェクト自体を動かさなくてもperspectiveを変化させるだけで面白い効果が期待できそうです。

値が小さいほどパースのかかりが強く出て、大きいほどパース感は弱まり制御しやすくなります。カメラの広角と望遠ですね。

きちんと体系化すれば3D空間のような見せ方も簡単にできるようになりそうだし、keyframesと組み合わせたら3D空間を動いているような見せ方もできそう……やりませんが。

そこまで行くともう3Dに適したものを使用したほうがよさそうです。

今回もCSSだから苦労した部分はたくさんあります。jsが使えればもっと楽に表現力豊かなものができたと思います。

しかし、CSSならではの手軽さはjsにはありません。

CSSでできることならCSSでやったほうが良い。

そのCSSでできることの可能性を探るという意味で今回のホバーアニメーション作成がみなさんにとっても意味あるものになっていると嬉しいです。

以上で今回の記事は終わりになります。
ツッコミやご意見等あればTwitter@happa1goまで。
(作ってからずっと放置でほぼ使ったことがないので反応等遅くなるかもしれませんが)

最後までご覧いただきありがとうございました。

この記事を気に入ったら

葉っぱ一号

葉っぱ一号

おいしいお店を探すのが好きです。おいしいお店のために遠出もしちゃいます。遠出したい衝動のためにおいしいお店を探すのかもしれません。そんなものですよね。

この人が書いた記事を見る >>