Vanilla JavaScriptとSVGでジニーエフェクトを実現する

2016-06-04

mac OSXのDockに格納されたアプリケーションウインドウを表示するときの拡大するときのジニーエフェクトをsnap.svgなどのライブラリを使用せずにVanilla JavaScriptとSVGで実現しました。

なにはともあれ、枠の中の"Click!"をクリックしてみてください

ジニーエフェクトのデモ

Click! Click! Click!
ダイアログなどのDIV

一応ChromeとSafari(OSX版、iOS9版)では動作を確認しましたが、おそらくIE(Edge含む)は動作しないと思います。

ちなみに、これを実現するためのJavaScriptコードは、minify + gzipでたったの 850bytes程度です。

デモのコード

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>SVG morphing</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1, user-scalable=no">
<style>
   #demo {
      height: 350px;
      border: 1px solid #999;
      margin: 20px auto;
      position: relative;
      background: #fff;
   }
   .btns {
      display: block;
      position: absolute;
      bottom: 0;
      border: 1px solid #999;
      border-radius: 4px;
      padding: .5em;
      cursor: pointer;
      background: #999;
   }
   #btn1 {
      left: 0;
   }
   #btn2 {
      left: 45%;
   }
   #btn3 {
      right: 10%;
   }
   #maindiv {
      width: 80%;
      margin: 20px auto;
      height: 250px;
      background: #aaa;
      position: relative;
      color: #fff;
      padding: 20px;
   }
   .close {
      position: absolute;
      top: -.5em;
      left: -.5em;
      width: 30px;
      height: 30px;
      border-radius: 15px;
      background: #f00;
      text-align: center;
      cursor: pointer;
   }
   #genie {
      position: fixed;
      top: 0;
      bottom: 0;
      left: 0;
      right: 0;
      fill: #999;
   }
</style>
</head>
<body>
   <div id="demo">
      <span class="btns" id="btn1">ボタン</span>
      <span class="btns" id="btn2">ボタン</span>
      <span class="btns" id="btn3">ボタン</span>
      <div id="maindiv">ダイアログなどのDIV</div>
   </div>
<script>
   var svgNS = "http://www.w3.org/2000/svg";

   // svg2.0に対応している場合にはcssでモーフィング
   // 対応していない場合にはsvgのanimate(SMIL)でモーフィング
   var isSVG2support = document.implementation.hasFeature("http://www.w3.org/TR/SVG2/feature#GraphicsAttribute", "2.0");

   var sBtn; // クリックされたbtnを格納
   var body = document.querySelector('body');
   var main = document.querySelector('#maindiv');

   // getBoundingClientRectで画面上の位置を取得するので
   // display:noneじゃダメ。事前に配置しておく必要あり。
   main.style.opacity = 0;

   // ジニーエフェクトのキモである漏斗形の形状を作成
   var funnel = function(btn){
      var b = btn.getBoundingClientRect();
      var m = main.getBoundingClientRect();
      // hはジニー画像の高さ
      var h = (b.bottom - m.top)*.9;
      // x1,y1は左上
      var x1 = m.left + m.width*.05;
      var y1 = m.top + h/9;
      // x2,y2は左下
      var x2 = b.left;
      var y2 = b.bottom -h/9;
      // x3,y3は右下
      var x3 = b.right;
      var y3 = y2;
      // x4,y4は右上
      var x4 = m.right - m.width*.05;
      var y4 = y1;
      // y5はベジェのアンカーポイントにあたる点のY
      var y5 = y1 + h/2;
      // template literalを使用しているので、古いブラウザはアウト
      // 改行は単に見やすくするためなので、1行で書いてもOK
      return `
         M ${x1},${y1}
         C ${x1},${y5} ${x2},${y5} ${x2},${y2}
         L ${x3},${y3}
         C ${x3},${y5} ${x4},${y5} ${x4},${y4}
         Z`;
   }

   // モーフィングするのに図形の頂点等を合わせる必要があるため
   // ベジェ曲線で最初と最後の四角形を作成
   var rect = function(elem){
      var b = elem.getBoundingClientRect();
      // funnelとは違って各直線とベジェ曲線を相対座標で描画
      // (行頭のアルファベットが小文字)
      // その方が四角形では書きやすかった
      return `
         M ${b.left},${b.top}
         c 0,0 0,${b.height} 0,${b.height}
         l ${b.width},0
         c 0,0 0,-${b.height} 0,-${b.height}
         Z`;
   }

   // ジニーエフェクトを実現する関数
   // btnはクリックされたボタンのdomエレメント
   // isExpandは拡大か縮小かを分けるフラグ
   var makeGenie = function(btn, isExpand){
      var svg = document.createElementNS(svgNS,"svg");
      svg.setAttribute('viewBox',`0 0 ${window.innerWidth} ${window.innerHeight}`);
      svg.id ='genie';
      body.appendChild(svg);
      var path = document.createElementNS(svgNS,"path");
      svg.appendChild(path);

      if(isSVG2support){
         // Chromeなどのsvg2.0サポートブラウザはsvgのpathを直接書き換えて
         // cssでアニメーション
         path.setAttribute('d',rect(isExpand ? btn : main));
         setTimeout(function(){
            path.style.transition = '.5s';
            path.setAttribute('d', funnel(btn));
         },10);
         setTimeout(function(){
            path.style.transition = '.5s cubic-bezier(.4, 1.6, .5, .8)';
            path.setAttribute('d',rect(isExpand ? main : btn));
         },500);
      } else {
         // safariなどはSMILでアニメーション
         var anime = document.createElementNS(svgNS,"animate");
         anime.setAttribute('attributeName', 'd');
         anime.setAttribute('repeatCount', '1');
         anime.setAttribute('dur', '1');
         anime.setAttribute('calcMode', 'spline');
         anime.setAttribute('keyTimes', '0; .5; 1');
         anime.setAttribute('keySplines', '0 0 1 1;.4 1.6 .5 .8');
         anime.setAttribute('fill', 'freeze');
         anime.setAttribute('values', `${rect(isExpand ? btn : main)};${funnel(btn)};${rect(isExpand ? main : btn)}`);
         path.appendChild(anime);
      }

      setTimeout(function(){
         body.removeChild(svg);
         if(isExpand){
            main.style.opacity = 1;
            // 縮小のための閉じるボタン
            var close = document.createElement('span');
            close.classList.add('close');
            close.textContent = 'x';
            close.addEventListener('click', shrink);
            main.appendChild(close);
         } else {
            sBtn.style.opacity = 1;
            sBtn = undefined;
         }
      },1100);
   }

   // ボタンがクリックされたときに拡大のジニーエフェクトを実行
   var expand = function(e){
      if(sBtn) return;
      var btn = e.target;
      btn.style.opacity = 0;
      sBtn = btn;
      makeGenie(btn, true);
   }

   // 閉じるボタンがクリックされたときに縮小のジニーエフェクトを実行
   var shrink = function(e){
      main.removeChild(e.target);
      main.style.opacity = 0;
      makeGenie(sBtn, false);
   };

   [].forEach.call(document.querySelectorAll('.btns'), function(elem){
      elem.addEventListener('click', expand);
   });
</script>
</body>
</html>

このサイトをPCでみた場合、左側に記事の一覧が表示されますが、その一覧をクリックすると今回のコードを多少いじった横向きのジニーエフェクトで各記事が表示されるようにしてみました。
ちょっと他にはない感じで斬新でしょう?

SVGJavaScript