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程度です。
デモのコード
<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でみた場合、左側に記事の一覧が表示されますが、その一覧をクリックすると今回のコードを多少いじった横向きのジニーエフェクトで各記事が表示されるようにしてみました。
ちょっと他にはない感じで斬新でしょう?