entry-header-eye-catch.html
entry-title-container.html

entry-header-author-info.html
Article by

PIXIV TECH FES.のLPを支えるCSSアニメーションテクニック

f:id:pxvpxv:20200120222328g:plain
※本記事に出てくるアニメーションは全てCSSで作られています

はじめまして、新卒エンジニアの yui540(@yui540)です。普段は、pixivFANBOXというサービスのCSSエンジニアをしています。

今回は、私がコーディング&ページ演出のアニメーションを担当させていただいたPIXIV TECH FES. の LP(第一弾)の CSSアニメーションの実装方法を一部解説します。

とその前に、「PIXIV TECH FES.って何?」という方もいると思うので、簡単にご説明します。

PIXIV TECH FES. は、私たちピクシブのエンジニアが普段からお世話になっている方や、 お話ししてみたい方をお招きして、サービス開発で得た技術的知見とピクシブの未来についてお話しするテックカンファレンスです。(去年の様子

また、PIXIV TECH FES. はイベントの情報を徐々に解禁していくため、LPは第一弾、第二弾...ようなバージョン分けをしています。そして各バージョンに応じて、ページのアニメーションも変えています。

f:id:pxvpxv:20200120161338g:plain
第一弾のオープニングアニメーション

f:id:pxvpxv:20200120161406g:plain
第二弾のオープニングアニメーション

CSSアニメーション活用例

実装の解説に移る前に、CSSアニメーションを活用できるパターンを4種類ご紹介します。

動画のようなオープニングアニメーション」「ローディングアニメーション」「UI の状態遷移アニメーション」「ループアニメーション」です。

  • 動画のようなオープニングアニメーション
    • 動画のような、一回きりのアニメーションを再生するもの
  • ローディングアニメーション
    • ページロード時のアニメーション
  • UIの状態遷移アニメーション
    • ユーザのインタラクションに応じて、UI をアニメーションさせながら状態遷移させるもの
  • ループアニメーション
    • 再生を始めてから、繰り返し再生され続けるアニメーション

f:id:pxvpxv:20200120202142g:plain
動画のようなアニメーション

f:id:pxvpxv:20200120161723g:plain
ローディングアニメーション

f:id:pxvpxv:20200120161803g:plain
UIの状態遷移アニメーション

f:id:pxvpxv:20200120202802g:plain
UIの状態遷移アニメーション

f:id:pxvpxv:20200120161858g:plain
ループアニメーション

このようにCSSアニメーションは、動画のような派手なフル画面アニメーションから細かなUIの状態遷移のアニメーションまで、幅広い使い方が出来ます。

まだまだ、いろんなパターンのアニメーションを見たいという方は私が日々更新しているアニメーションスニペットをご覧ください。

PIXIV TECH FES.のLPの重要ポイントのみを解説

さて、本題に戻ります。

CSSアニメーションのコードは基本的に冗長になりがちなので、PIXIV TECH FES. のLP(第一弾)の実装で私が重要だと思う部分を一部解説します。

ボヤけた視界の表現

f:id:pxvpxv:20200120182545g:plain

今回、解説するポイントは ↑ のオープニングアニメーションで使われた「ボヤけた視界の表現」です。

まず、「ピクシブテックフェス」と書かれたロゴにぼかしがかかっている箇所ですが、これは単にfilter: blur()をアニメーションさせるのではなく、下記のような実装の工夫がなされています。

f:id:pxvpxv:20200120182834p:plain

filter: blur(4px);(.logo--weak-blur)とfilter: blur(12px);(.logo--strong-blur)をかけた要素を二つ用意し、その二つをposition: absolute;で重ねます。

そして、@keyframesopacity(filter: opacity()でも代用可)の値を0➡︎1➡︎0に変化させるfadeInOutを定義し、.logo--weak-blur.logo--strong-blurを交互に表示・非表示させています。

<div class="stage">
  <img class="logo--weak-blur" src="..." />
  <img class="logo--strong-blur" src="..." />
</div>
.stage {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
}

[class^="logo--"] {
  display: block;
  position: absolute;
  top: 50%;
  left: 50%;
  width: 480px;
  transform: translate(-50%, -50%);
  /* アニメーション */
  animation: fadeIn 0.5s ease-in-out 0.2s both;
}

.logo--weak-blur {
  filter: blur(4px);
  /* 始点と終点を反転 */
  animation-direction: reverse;
}

.logo--strong-blur {
  filter: blur(12px);
}

@keyframes fadeInOut {
  from,
  to {
    opacity: 0;
  }
  50% {
    opacity: 1;
  }
}

また、animation-direction: reverse;でキーフレームの再生方向を反転させて、.logo--weak-blur.logo--strong-blurそれぞれ交互に表示されるように調整しています。このようにanimation-directionを使うことでキーフレーム定義を節約することが出来ます。

注意としては、animation-direction: reverse;すると、イージング(animation-timing-function)も反転してしまいます。

filter: blur();をそのままアニメーションさせないのは、transitionanimationプロパティでアニメーションさせた場合、著しくパフォーマンスが悪いからです。ブラウザ間の描画差異、アニメーションのコマ落ちなどが起きてしまうため、ぼかしが弱い要素と強い要素を交互に表示・非表示する方法で描画コストを下げています。

もう少しチューニングするとすれば、ぼかしをfilter: blur();でするのではなく、Photoshop 等で事前にロゴをぼかした画像を用意しておくことで、filter: blur();分の描画コストを節約することができます。

f:id:pxvpxv:20200120184246p:plain

次にまぶたの表現ですが、これは二つのdiv要素で構成されています。(擬似要素::before, ::afterでも代用可)

<div class="stage">
  <div class="eye--top"></div>
  <div class="eye--bottom"></div>
</div>
.stage {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
}

[class^="eye--"] {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: calc(50% + 120px);
}

.eye--top {
  background: linear-gradient(to bottom, black calc(100% - 120px), transparent);
  /* アニメーション */
  animation: eyeTop 0.6s cubic-bezier(0, 0, 0.11, 0.99) 0.2s forwards,
    eyeTop 0.22s cubic-bezier(0, 0, 0.11, 0.99) 1.2s reverse forwards,
    eyeTop 0.22s cubic-bezier(0, 0, 0.11, 0.99) 1.42s forwards,
    eyeTop 0.38s cubic-bezier(0, 0, 0.11, 0.99) 6.2s reverse forwards,
    eyeTop 0.5s cubic-bezier(0, 0, 0.11, 0.99) 6.58s forwards;
}

.eye--bottom {
  bottom: 0;
  background: linear-gradient(to bottom, black calc(100% - 120px), transparent);
  /* アニメーション */
  animation: eyeBottom 0.6s cubic-bezier(0, 0, 0.11, 0.99) 0.2s forwards,
    eyeBottom 0.22s cubic-bezier(0, 0, 0.11, 0.99) 1.2s reverse forwards,
    eyeBottom 0.22s cubic-bezier(0, 0, 0.11, 0.99) 1.42s forwards,
    eyeBottom 0.38s cubic-bezier(0, 0, 0.11, 0.99) 6.2s reverse forwards,
    eyeBottom 0.5s cubic-bezier(0, 0, 0.11, 0.99) 6.58s forwards;
}

@keyframes eyeTop {
  from {
    transform: translateY(0);
  }
  to {
    transform: translateY(-101%);
  }
}

@keyframes eyeBottom {
  from {
    transform: translateY(0);
  }
  to {
    transform: translateY(101%);
  }
}

linear-gradient(to bottom, ...);で端をグラデーションでぼかすことでまぶたっぽさを表現しています。

@keyframestransform: translateY();で上下それぞれのまぶたを動かすeyeTop, eyeBottomキーフレームを定義します。

そして、まぶたを複数回動かすためにanimationプロパティをカンマ(,)区切りで指定する技を使います。

animationプロパティには、カンマ(,)区切りでキーフレーム等を渡すことでアニメーションを並列に走らせたり、直列に走らせたりすることができます。

今回は、まぶたの閉じ開き、合わせて 5回動かしているので、5個キーフレームをカンマ区切りで指定しています。

これで、ボヤけた視界の表現が完成です。

いかがでしょうか?とてもシンプルな実装で質感のある表現ができたかと思います。

おまけ: React Hooksを使ったスクロールアニメーションの実装パターン

おまけとして、今回のPIXIV TECH FES. の LPReact + styled-componentsでコーディングされているのですが、どのようにしてスクロールアニメーションを発火させたのかをご紹介します。

スクロール検知は下記のような簡単にIntersectionObserverをラップしたhooksを作っています。

ファーストビューで画面に対象要素が入っていても、スクロールに合わせてアニメーション発火させたいので、初回スクロールのみscrollイベントを使っています。

/**
 * 要素が画面に入ったかを検知する
 * ファーストビューで要素が画面に入っていてもスクロールするまでは発火しないように
 * 初回のスクロールのみscrollイベントを使う
 */
export const useIntersectionObserver = (): [
  React.RefObject<HTMLElement>,
  boolean
] => {
  const targetRef = useRef<HTMLElement>(null);
  const [isIntersected, setIsIntersected] = useState(false);

  useEffect(() => {
    if (targetRef.current === null) {
      return;
    }

    const observer = new IntersectionObserver(
      changes => {
        changes.forEach(change => {
          if (change.isIntersecting) {
            setIsIntersected(true);
            observer.disconnect(); // 一度画面に入ったら監視をやめる
          }
        });
      },
      { rootMargin: "0px 0px -200px" }
    );

    const handleScroll = () => {
      if (targetRef.current === null) {
        return;
      }

      observer.observe(targetRef.current);
      window.removeEventListener("scroll", handleScroll);
    };
    window.addEventListener("scroll", handleScroll);

    return () => observer.disconnect();
  }, [diff]);

  return [targetRef, isIntersected];
};

useIntersectionObserverは下記のように、画面内に入ったかどうかを検知したい要素にrefを渡し、画面内に対象の要素が入ったら、isIntersectedtrueになり、アニメーションが発火される仕組みになっています。

function HogeComponent() {
  const [wrapperRef, isIntersected] = useIntersectionObserver();

  return (
    <Wrapper ref={wrapperRef} isIntersected={isIntersected}>
      <h1>テキスト</h1>
    </Wrapper>
  );
}

const Wrapper = styled.div<{ isIntersected: boolean }>`
  /* 画面内に入ったらアニメーションを発火する */
  ${({ isIntersected }) =>
    isIntersected &&
    css`
      animation: fadeIn 0.4s ease-out 0.2s forwards;
    `}
`;

さいごに

いかがでしたでしょうか?

普通であれば、WebGLHTML5Canvasを使うような派手なアニメーションも、使い方次第では CSSアニメーションで実現可能です。

慣れてくれば、このような面白い動きも簡単に作れるようになります。

f:id:pxvpxv:20200120190827g:plain

f:id:pxvpxv:20200120190859g:plain

f:id:pxvpxv:20200120190912g:plain

f:id:pxvpxv:20200120191003g:plain

f:id:pxvpxv:20200120203127g:plain

また、CSSアニメーションの強みとして、レスポンシブ対応のフル画面アニメーションをライブラリやプラグインに頼ることなく、手軽に実装できることです。

これを機会に皆さんも CSSアニメーション芸を始めてみませんか?

また、私はPIXIV TECH FES. のSHORT SESSION に登壇予定です。 「動き」のある Web サイトを支える CSSアニメーション技術と題して、PIXIV TECH FES. の LP(第二弾)ついてお話ししますので、当日来られる方はお楽しみに!

ピクシブ株式会社では、CSSアニメーションが大好きなエンジニアを中途・新卒共に募集しています!詳細については、ピクシブ採用サイトをご覧ください。

それでは今回はこの辺りで。 PIXIV TECH FES.で皆さんとお会いできることを楽しみにしています!さようなら!

f:id:pxvpxv:20200120204137p:plain

icon
yui540
2019年4月新卒入社。クリエイター事業部FANBOX部所属。pixivFANBOXでCSSエンジニアをしています。特技はCSSアニメーションを使って、バチバチに動くWebサイトを作ること。