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