注目の記事

HTML5 × CSS3 × jQueryを真面目に勉強 – #11 Path風サークルメニューを作ってみた

2013.01.07

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

cap_path

そんな訳で、写真共有SNSの一つであるPathにある、あのサークルメニューを再現してみたので、ここにその手順をまとめておくとします。

Pathはネイティブアプリなので、JavaないしObjective-Cにて実装されていますが、こちとらはそんなハイソなテクニックは使わずに、JavaScriptとCSS3だけで行けるところまで行ってみます。

はじめに

とりあえずサークルメニューの要件を大まかに書きだしてみました。

  1. トグルボタンをクリックしてメニューアイテムの表示/非表示を切り替えたい
  2. 各メニューは円周上に均等に配置された状態で表示させたい
  3. 表示/非表示はアニメーションで切り替わるようにしたい
  4. メニュー数の増減には柔軟に対応できるようにしたい
  5. その他、各メニューの間隔や角度、表示時の距離などはオプショで指定できるようにしたい

ひとまずこんなもんで良いでしょう。次にこれらの要件をどのように実装するかの方針を決めます。

  1. 各メニュー表示時の配置はJavaScriptを使って動的に計算する
  2. 表示/非表示のアニメーションはCSS Transition と Animations を組み合わせて実現する
  3. jQueryプラグインとして機能を外出ししたい

先に今回作成するプラグインのデモを御覧ください。オプションのパラメータを設定して、どのような動きをするか確認することができます。

sampleimg_circlemenu-demo1

では一つずつ解説しながら実装していきます。

Path風サークルメニューを実装

1 | HTMLをマークアップ

まずはHTMLから書いていきます。

circlemenu.html

<div class="circlemenu-container">
	<a href="#" class="menu-button" title="Toggle"><span></span></a>
	<ul class="menu-option">
		<li><a href="#" title="item1"><span class="icon camera">Item1</span></a></li>
		<li><a href="#" title="item2"><span class="icon home">Item2</span></a></li>
		<li><a href="#" title="item3"><span class="icon search">Item3</span></a></li>
		<li><a href="#" title="item4"><span class="icon music">Item4</span></a></li>
		<li><a href="#" title="item5"><span class="icon copy">Item5</span></a></li>
		<li><a href="#" title="item6"><span class="icon user">Item6</span></a></li>
	</ul>
</div>

極力シンプルなマークアップを目指しました。表示/非表示を切り替えるトグルボタンをaタグで、各メニューをulタグでリスト状にマークアップしました。さらにこれらをdivタグで囲うことで、全体をパッケージ化しました。

各メニューはアイコンで表示するために、spanタグでiconクラスの要素を定義しています。

2 | トグルボタンをCSSでデザイン

トグルボタンのデザインを書いていきます。

circlemenu.css

.menu-button {
  background: #000;
  border: 4px solid #fff;
  border-radius: 50%;
  bottom: 0;
  box-shadow: 0 0 24px rgba(58, 58, 58, 0.6);
  display: block;
  height: 36px;
  left: 0;
  overflow: hidden;
  position: absolute;
  text-indent: 100%;
  white-space: nowrap;
  z-index: 9999;
  background-image: linear-gradient(#db3a2b, #cf0404);
}
.menu-button span {
  background: url(../img/cross.png) no-repeat center center;
  display: block;
  height: 36px;
  width: 36px;
  transition: 0.3s ease;
}

.active .menu-button span {
  transform: rotate(-135deg);
}

当記事執筆時点(※2013年1月)ではグラデーション、Transitionsプロパティに対してベンダープレフィックスをつけて指定する必要があります。当記事では可読性を考慮して省略しております。

トグルボタンのアイコンは画像を使用していますが、それ以外はCSSで表現しています。また、rotate関数()を用いてメニュー表示(※アクティブ状態)時にトグルボタンが回転する処理を指定します。

3 | メニューをCSSでデザイン

次に各メニューのデザインを書いていきます。

.menu-option li {
  display: block;
  left: 0;
  position: absolute;
  top: 0;
  transition: 0.3s;
}
.menu-option li a {
  background-color: #333;
  background-size: #900;
  border: 4px solid #fff;
  border-radius: 50%;
  display: block;
  height: 16px;
  padding: 8px;
  position: relative;
  width: 16px;
  transition-duration: 0.2s;
}
.menu-option li a span {
  overflow: hidden;
  text-indent: 100%;
  white-space: nowrap;
  transition: 0.4s ease;
}
.menu-option li a:hover {
  box-shadow: 0 0 18px #5bdaec, 0 0 12px #4b78cb, inset 0 0 4px #f8f8f3;
}

これでメニューの基本的なデザインができました。加えて各メニューに表示するアイコンのスタイルを指定します。

icon {
  background: url(../img/mimiGlyphs.png) no-repeat;
  background-size: 164px 128px;
  display: block;
  height: 16px;
  width: 16px;
}
.icon.camera     { background-position: -32px 0; }
.icon.home       { background-position: -80px 0; }
.icon.search     { background-position: -96px 0; }
.icon.music      { background-position: -112px 0; }
.icon.copy       { background-position: 0 -17px; }
.icon.user       { background-position: -32px -17px; }
.icon.picture    { background-position: -112px -17px; }
.icon.gear       { background-position: -128px -17px; }
.icon.cloud      { background-position: -112px -32px; }
.icon.smartphone { background-position: -144px -32px; }
.icon.game       { background-position: -80px -80px; }
.icon.email      { background-position: -96px -80px; }
.icon.calendar   { background-position: -144px -96px; }
.icon.video      { background-position: -16px -112px; }
.icon.facebook   { background-position: -80px -112px; }
.icon.twitter    { background-position: -96px -112px; }

3 | JavaScriptを実装

jQueryプラグイン化するという話はひとまず置いておいて、まずは動くものを作ってしまうとします。

$(function() {
	var angleDifference = 90;
	var startAngle = 0;
	var radius = 200;

	var $obj = $('.circlemenu-container');
	var menuButton = $obj.children('.menu-button');
	var menuItems = $obj.children('.menu-option').children();
	var itemAngle = [];
	var xPos = [];
	var yPos = [];
	var angle = angleDifference / (menuItems.length - 1);

	menuButton.unbind('click');

	function clickHandler(e) {
		e.preventDefault();
		if ($(this).parent().hasClass('active')) {
			$(this).parent().removeClass('active').addClass('inactive');
			setPosition(false);
		} else {
			$(this).parent().removeClass('inactive').addClass('active');
			setPosition(true);
		}
	}

	function setPosition(value) {
		menuItems.each(function(i, item) {
			var ele = $(this);
			delayTime = i * delay;
				$(item).css({
					'left': value ? xPos[i] : 0,
					'top' : value ? yPos[i]* -1: 0
				});
		});
	}

	menuButton.bind('click', clickHandler);

	menuItems.each(function(i, item) {
		itemAngle[i] = (startAngle + angle * i) * Math.PI / 180;
		xPos[i] = radius * Math.cos(itemAngle[i]);
		yPos[i] = radius * Math.sin(itemAngle[i]);
		$(item).css({'transform': 'rotate(' + (90-itemAngle[i]*180/Math.PI) + 'deg)'});
	});
});

筋金入りの文系である僕としては三角関数と聞いただけでパニックに陥るわけですが、オブジェクトをサークル状に配置する上で避けては通れなかった為、今回はとりあえずこんな感じで実装しました。

ここまでで、ひとまず動くものはできました。Webブラウザで確認してみてください。トグルボタンをクリックすると各メニューが飛び出して円周上に等間隔で配置されて表示されるはずです。

対象ブラウザはChrome、Safari、Firefox、Opera となります。IE9ではアニメーションが実行されません。

4 | アニメーションにディレイ効果をつける

本家Pathのメニューをよく見てみると、メニューが同じタイミングで飛び出すのではなく、少しずつ時間差を置いて一つずつ順番に出てきています。そんな訳で、その動きを再現してみるとします。

コードを以下のように書き換えます。

var delay = 40;

function setPosition(value) {
	menuItems.each(function(i, item) {
		var ele = $(this);
		delayTime = i * delay;
		window.setTimeout(function() {
			$(item).css({
				'left': value ? xPos[i] : 0,
				'top' : value ? yPos[i]* -1: 0
			});
		}, delayTime);
	});
}

setTimeout()関数を使ってループ処理に間隔をおくようにしました。変数delayの値を大きくすると間隔が長くなります。

5 | アニメーションにバウンス効果をつける

メニューが飛び出した際にバウンスするような効果をつけてみるとします。jQueryでもいいんですけど…、
でも僕はCSS3アニメーション!(`・ω・´)

以下のコードを追記します。

.active .menu-option li a {
  animation: expand 0.7s ease 0s backwards;
}

@keyframes expand {
  0%   { top: 0; }
  50%  { top: -10px; }
  70%  { top: 10px; }
  100% { top: 0; }
}

CSS3 Animationsでtopプロパティを動かしてアニメーション指定しています。これでメニューが飛び出したアニメーションの最後にバウンス効果が出るようになりました。

6 | jQueryプラグイン化する

ここまでで必要なロジックは出来ているので、あとはこれをプラグイン化して外出しするとします。

jquery.circlemenu.js

;(function($) {
	$.fn.circlemenu = function(options) {
		var elements = this;
		var opts = $.extend({}, $.fn.circlemenu.defaults, options);

		elements.each(function() {
			var $obj = $(this);
			var menuButton = $obj.children('.menu-button');
			var menuItems = $obj.children('.menu-option').children();
			var itemAngle = [];
			var xPos = [];
			var yPos = [];
			var angle = opts.angleDifference / (menuItems.length - 1);

			menuButton.unbind('click');

			function clickHandler(e) {
				e.preventDefault();
				if ($(this).parent().hasClass('active')) {
					$(this).parent().removeClass('active').addClass('inactive');
					setPosition(false);
				} else {
					$(this).parent().removeClass('inactive').addClass('active');
					setPosition(true);
				}
			}

			function setPosition(value) {
				menuItems.each(function(i, item) {
					var ele = $(this);
					delayTime = i * opts.delay;
					window.setTimeout(function() {
						$(item).css({
							'left': value ? xPos[i] : 0,
							'top' : value ? yPos[i]* -1: 0
						});
					}, delayTime);
				});
			}

			menuButton.bind('click', clickHandler);

			menuItems.each(function(i, item) {
				itemAngle[i] = (opts.startAngle + angle * i) * Math.PI / 180;
				xPos[i] = opts.radius * Math.cos(itemAngle[i]);
				yPos[i] = opts.radius * Math.sin(itemAngle[i]);
				$(item).css({'transform': 'rotate(' + (90-itemAngle[i]*180/Math.PI) + 'deg)'});
			});

		});

		return this;
	};

	// default options
	$.fn.circlemenu.defaults = {
		startAngle: 0,
		angleDifference: 90,
		radius: 200,
		delay: 40
	};

})(jQuery);

これでプラグイン化できました。jQueryプラグインについては以下の記事にて詳しく解説しています。

最後に呼び出し側です。以下のようにオプション指定して呼び出します。

circlemenu.html (※JS部分)

$(function() {
	$('.circlemenu-container').circlemenu({
		startAngle: 30,
		angleDifference: 300,
		radius: 200,
		delay: 40
	})
		.find('.menu-option')
			.find('a').click(function(e) {
				e.preventDefault();
				alert($(this).text());
			});
});

sampleimg_circlemenu1

これで完成です。こちらから実際の動きを確認できます。

おわりに

CSS3のアニメーション機能のお陰で、このようにユニークなインタラクションのメニューも割りと簡単に再現できました。モバイル端末向けに特化されたインタラクションですが、メニューという用途に拘らずこのインタラクションそのものに注目すれば、PC向けに面白い表現ができるのではないでしょうか。