technical

【備忘録】SVGのpathをJavaScriptでぬるぬる動かしたかった

ぬるんぬるん!ぐねんぐねん!

こちらは、最近流行りのSVG!gif画像ではありません。
クリックして色を変えるなどの処理がリアルタイムで行えます。
IEやsafariなど一部のブラウザではうまく動作しないかもしれません。ゴメンナサイ。
これをjavascriptで制御しようとしたところ、いろいろ大変だった話。

※この記事は、SVGが動作する環境の方へ向けて書いています。動かない方はゴメンナサイ。

SVGは、ドットの集まりではなく、パス(点と線)の集まりである為、解像度は無限大です!
また、パスの値は数値で書かれていて、ページが表示されてからでも、パスの値を調節すれば、見た目をぐにぐに変えちゃったりもできます!
ただし、現時点ではブラウザによって対応していない環境があったり、大きさの指定がややこしかったりと、いろいろ課題もございます。

今回は、どうしてもSVGをぬるぬる動かしたい!という案件がございまして、工数はかかりましたが、なんとか要件を満たすことができました。
拙いやり方でしたが、とりあえず備忘録として書いておきます。

今回は、こちらのSVGを例にご説明いたします。
このSVGのソースはこちらになります。
	<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px"
	 y="0px" viewBox="0 0 180 180" enable-background="new 0 0 180 180" xml:space="preserve">
		<path fill="#88BB33" d="M124,89c0,27-22,49-49,49s-49-22-49-49S47,40,75,40.6S124,62,124,89z" >
		</path>
	</svg>
	
これを別のhtmlページにコピペすると、上に表示されている緑色の円がそっくりそのまま表示されるはずです。
とてもシンプルな構造ですよね!

このソースの中には、図形を表すための図形の頂点の位置図形の線の太さや塗りつぶしの色、などが書かれています。
20150313_1
ブラウザはこれを読んで描画を行うわけです。
例えば、この例の場合、円の形(点と線の集まり)を表しているのはこの部分です。↓
d=”M124,89c0,27-22,49-49,49s-49-22-49-49S47,40,75,40.6S124,62,124,89z”
そして、この中に書かれている点の位置を書きかえてしまえば、その場で表示を変えることができる、という寸法です。

ここまで前置き。詳しい説明に移ります。



SVG要素を動的に変更したい場合は、animateタグを使用する、もしくはJavaScriptで直接値を打ち込む、などの方法がございます。
まずは、animateタグについて。
animateタグの中身には、変更したい要素名と、動作スピード動作条件などを記載します。
		<animate attributeName="fill" 
					values="#88BB33;#CC6699"
					dur="2.0s"
					repeatCount="indefinite" >
	
上記の例では、
[attributeName]…「fill」という要素、この場合fillなので塗りつぶしの色を、
[values]…「#88BB33;#CC6699」すなわち#88BB33から#CC6699に、
[dur]…「2.0s」2秒間かけて変化させる。という命令が書いてあります。
また、繰り返し回数[repeatCount]…「indefinite」で、アニメーションを無限に繰り返すという指定をしました。

このタグをどこに書くかというと、アニメーションさせたいSVG要素の内側に書きます。
こんな感じ。
	<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px"
	 y="0px" viewBox="0 0 180 180" enable-background="new 0 0 180 180" xml:space="preserve">
		<path fill="#88BB33" d="M124,89c0,27-22,49-49,49s-49-22-49-49S47,40,75,40.6S124,62,124,89z" >
			<animate attributeName="fill" 
					values="#88BB33;#CC6699"
					dur="2.0s"
					repeatCount="indefinite" >
		</path>
	</svg>
	

これを実際に実行したものがこちら。
色が緑から赤にだんだんと変わっています。
延々と変わっています。

これくらいなら簡単な変化でありますが、このanimateタグは、pathの形状の変化にも使えてしまうのです。

そのためには、pathについて「動作前の状態」「動作後の状態」を用意する必要があるわけです。
ということでAdobe Illustratorにてパス形状を編集する画面を例にいたします。
(SVGを書き出せるソフトウェアでしたら、他のものでも構いません。)
こちらが、動作前の状態とします。
20150313_2
次が、動作後の状態とします。
20150313_3
それぞれ、別名で保存からsvgとして保存します。
20150313_4
そして、保存したSVGファイルをテキストエディタで開きます。
図形の形状を表すpathの中の“d”の値がそれぞれ違うことが確認できます。
20150313_5
動作前の”d”:
M124,89c0,27-22,49-49,49s-49-22-49-49S47,40,75,40.6S124,62,124,89z

動作後の”d”:
M154,89c0,27-52,49-79,49s-49-22-49-49S47,40,75,40.6S154,62,154,89z

この”d”の中身の値を抽出して、animateタグに打ち込めば良いわけです!
animateタグの[attributeName]の中身を、図形の形状を表す“d”に変更し、
実際に変化する値である、[values]の中身に動作前、動作後のパス形状を”;”セミコロンで区切って入力。
こんなタグになりました。
	<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px"
	 y="0px" viewBox="0 0 180 180" enable-background="new 0 0 180 180" xml:space="preserve">
		<path fill="#88BB33" d="M124,89c0,27-22,49-49,49s-49-22-49-49S47,40,75,40.6S124,62,124,89z" >
			<animate attributeName="d" 
					values="M124,89c0,27-22,49-49,49s-49-22-49-49S47,40,75,40.6S124,62,124,89z;
							M154,89c0,27-52,49-79,49s-49-22-49-49S47,40,75,40.6S154,62,154,89z"
					dur="1.2s"
					repeatCount="indefinite" >
		</path>
	</svg>
	
動作させてみます。
ぬるっと動きました!
しかし、このままでは延々とアニメーションしてしまいます。
そこで、アニメーションの動作を制御するbegin要素を追加します。
begin=”indefinite”とすることで、animateの動作タイミングをJavaScriptで制御できます。
さらに繰り返し回数[repeatCount]…「1」と設定することで、動作回数を1度だけとします。
そしてこのanimateタグにidを付与します。
また、この後の動作のためにpathのタグにもidを付与しておきます。
	<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px"
	 y="0px" viewBox="0 0 180 180" enable-background="new 0 0 180 180" xml:space="preserve">
		<path id="circle_path" 
			fill="#88BB33" d="M124,89c0,27-22,49-49,49s-49-22-49-49S47,40,75,40.6S124,62,124,89z" >
			<animate attributeName="d" 
					values="M124,89c0,27-22,49-49,49s-49-22-49-49S47,40,75,40.6S124,62,124,89z;M154,89c0,27-52,49-79,49s-49-22-49-49S47,40,75,40.6S154,62,154,89z"
					dur="1.2s"
					repeatCount="1" 
					begin="indefinite" 
					id="circle_animate" >
		</path>
	</svg>
	
そして、JavaScriptからidをもとにanimateタグをゲットし、
beginElement();を呼ぶことで、アニメーションを動作させることができます!
		//アニメを動作させる
		var animate = document.getElementById('circle_animate');
		animate.beginElement();
	
ボタンクリックで、beginElement();を呼ぶようにしてみました。
クリック!

クリックでアニメを動かすことに成功!ひとつ前に進んだ!

ただし、このままではアニメーションしても元に戻ってしまいます。
何故かというと、表示されているSVGの形状は、pathの場合”d”の中身で決まっていて、
アニメーションした後はその値による表示に戻ってしまうためです。
そこで今度は、アニメーションしたあとにpath要素自体の”d”の値をJavaScriptで書き換えてしまうようにします。
		//アニメを動作させる
		var animate = document.getElementById('circle_animate');
		animate.beginElement();
		//pathそのものの形状を変化させる
		var path = document.getElementById('circle_path');
		path.setAttribute('d','M154,89c0,27-52,49-79,49s-49-22-49-49S47,40,75,40.6S154,62,154,89z');
	
むくり!

アニメ後も、要素の形状がそのままになりました!

ここで、疑問が生まれます。 動作前と後で、どんなパスを入れてもちゃんとアニメーションしてくれるのか?という問題です。
ためしに、単純なパスと複雑なパスをanimateタグで繋げてみます。
20150313_6
20150313_7
	<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px"
	 y="0px" viewBox="0 0 180 180" enable-background="new 0 0 180 180" xml:space="preserve">
		<path fill="#88BB33" d="M150.8,93l-58,62.2L29.2,95.8C44,79,69.1,51.3,87.2,33.6L150.8,93z" >
			<animate attributeName="d" 
				values="M150.8,93l-58,62.2L29.2,95.8C44,79,69.1,51.3,87.2,33.6L150.8,93z;
						M112.9,90.5l16.5,32.3l-35.7-6.5L68.2,142l-4.9-36L30.9,89.6c13.9-5.7,19.4-9.5,32.7-15.7L69.2,38l25.1,26.3l35.8-5.8L112.9,90.5z"
                dur="1.2s"
                repeatCount="indefinite"" >
		</path>
	</svg>
	
あれ。ぬるぬるしない。
調査の結果、どうやら動作前と後で、「パスを表す構成」が同じでなければうまく動いてくれないようです。
具体的には、点の数が変わったり、座標の指定方法が変わったりすると、ダメみたいです。
たとえば頂点が4つしか無い四角形から、頂点が5つある五角形に変形、ということはできないようです。
ただし、「頂点が5つだけど最初だけ四角形に見えるパス」などを用意すれば、その限りではありません。
20150313_8

さて、脱線しましたが、animateタグは「動作前」と「動作後」だけでなく、一つのタグでいくつも動作をつなげることができます。
変化させたいパスを“;”セミコロンでどんどん繋げていけば、こんな動きもできちゃうわけです!
↑クリックすると色が変わります(笑)
<animate dur="2.4s" attributeName="d" 
values="M152.7,128.7c0,27.2-22.1,49.4-49.3,49.4S54,155.9,54,128.7s22.1-49.3,49.3-49.3S152.7,101.4,152.7,128.7z;
M152.7,128.7c0,27.2-22.1,49.4-49.3,49.4S54,155.9,54,128.7s22.1-49.3,49.3-49.3S152.7,101.4,152.7,128.7z;
M152.7,128.7c0,27.2-22.1,49.4-49.3,49.4S54,155.9,54,128.7s72.1-49.3,99.3-49.3S152.7,101.4,152.7,128.7z;
M202.7,128.7c0,27.2-72.1,49.4-99.3,49.4s0.7-22.2,0.7-49.4s72.1-49.3,99.3-49.3S202.7,101.4,202.7,128.7z;
M222.7,158.7c0,27.2-52.1,19.4-79.3,19.4s-19.3-52.2-19.3-79.4s52.1-19.3,79.3-19.3S222.7,131.4,222.7,158.7z;
M316.7,65c0,27.2-132.1,109.4-159.3,109.4s10.7-62.2,10.7-89.4s82.1-59.3,109.3-59.3S316.7,37.7,316.7,65z;
M392.7,85c0,27.2-132.1,29.4-159.3,29.4s32.7-36.2,32.7-63.4s50.1-25.3,77.3-25.3S392.7,57.7,392.7,85z;
M447.7,84c0,27.2-94.1,13.4-121.3,13.4s-27.2-15.3-15.3-57.4s50.1-25.3,77.3-25.3S447.7,57.3,447.7,84.6z;
M498.7,78.6c0,27.2-24.1,63.4-51.3,63.4s-27.2-30.2-65.3-57.4s0.1-45.3,27.3-45.3 S498.7,51.3,498.7,78.6z;
M534.7,96.9c0,27.2-4.1,78.4-31.3,78.4s-27.2-32.2-67.3-59.4s-5.9-53.3,21.3-53.3 S534.7,69.7,534.7,96.9z;
M558.7,130.6c0,27.2-25.1,47.4-52.3,47.4s-55.3-15.2-55.3-42.4s4.1-47.3,31.3-47.3 S558.7,103.3,558.7,130.6z;
M559.7,130.6c0,27.2-25.1,47.4-52.3,47.4s-52.3-20.2-52.3-47.4s24.1-49.3,51.3-49.3 S559.7,103.3,559.7,130.6z;
M152.7,128.7c0,27.2-22.1,49.4-49.3,49.4S54,155.9,54,128.7s22.1-49.3,49.3-49.3S152.7,101.4,152.7,128.7z"
repeatCount="indefinite" >
	
さらに、複雑な動作をさせたい方には、keyTime、keySplinesという、アニメーションのスピードを細かく設定できる要素もあります!
今回は少々手間のかかる方法でしたが、もっと簡単に動かせる仕組みが一般化していくとよいですね!

  • Category: technical / svg
  • Posted: 2015/3/18 17:10
  • Author: ユーリ
technical

【jQuery】コピペOK!タッチ操作とマウス操作の動作定義をまとめる

さいきん、スマホでもPCでも同じような動作を求められるWebAPIが増えていますよね。
動作はjQueryで書くことになるのですが、もともと用意されているイベント(click,mousemoveなど)ではタッチ操作を拾いきれないことがあります。
そこで、二つの環境の動作を同じ関数にまとめる定義を作りました。


まず、こちらがサンプルボタンです!PCでもスマホでも共通のコードで動作するところがポイントです!
demo

基本のクリック動作、押下、移動、キャンセルの4種を定義できます。

サンプルコードです!
動作定義部分と、jQuery拡張部分の二つに分けてご紹介します。

まずは、動作定義部分です。
上に置いてあるボタンは、こちらのコードで動作しています。
$(function(){
	$('#hoge').touchInterface(
	function(e,$_this){ //タッチorクリックの動作
		$_this.empty().append('action:up/end');
		$_this.css('background-color','#FFF');
	},
	function(e,$_this){ //(オプション)押下
		$_this.empty().append('action:down/start');
		$_this.css('background-color','#AAF');
	},
	function(e,$_this){ //(オプション)移動
		$_this.empty().append('action:move');
		$_this.css('background-color','#AFA');
	},
	function(e,$_this){ //(オプション)範囲外
		$_this.empty().append('action:out/cancel');
		$_this.css('background-color','#FFA');
	}
	);
});
はい、わりとシンプルな作りですね!
jQueryエレメントの挙動を拡張して”touchInterface”というアクションを命令し、
その引数として関数を4つ渡しています。
まとめるとこんな感じです。
$(element).touchInterface(up,[down],[move],[out]);
引数はすべてfunction(関数)です。
また、2〜4つ目の引数は省略できます。

関数内では、 $(this) という表記が使えない為、代わりに $_this を使用して下さい。
また、タッチ位置などを使いたい場合は、e の中から引っ張って下さい。

続いて、肝心のjQuery拡張部分です。
jQueryの機能を拡張して、「touchInterface」というアクションを定義しています。
$.fn.extend({
	touchInterface : function(up){
		var down	= (arguments.length > 1 ) ? arguments[1] : false;
		var move	= (arguments.length > 2 ) ? arguments[2] : false;
		var cancel	= (arguments.length > 3 ) ? arguments[3] : false;
		return $(this).on({
			'touchstart mousedown'	: function(e){
				e.preventDefault();
				this.touching = true;
				if(down)down(e,$(this));
			},
			'touchmove mousemove'	: function(e) {
			 	if(!this.touching)return;
				e.preventDefault();
				if(move)move(e,$(this));
				if('ontouchstart' in window){
					var touch = e.originalEvent.changedTouches[0];
					var offset = $(this).offset();
					if(	touch.pageX < offset.left || touch.pageX > $(this).outerWidth()  + offset.left ||
						touch.pageY < offset.top  || touch.pageY > $(this).outerHeight() + offset.top  ){
						this.touching = false;
						if(cancel)cancel(e,$(this));
					}
				}
			},
			'touchcancel mouseout'	: function(e){
				if(!this.touching)return;
				e.preventDefault();
				if(cancel)cancel(e,$(this));
				this.touching = false;
			},
			'touchend mouseup'		: function(e){
				if(!this.touching)return;
				e.preventDefault();
				up(e,$(this));
				this.touching = false;
			}
		}).css('cursor','pointer');
	}
});
上記のjQuery拡張部分のコードは、JavaScriptソース内のjQuery動作定義部分の外側に記述してください。
もちろんjQuery本体も忘れずに読み込んでくださいね!

拡張部分のポイントは、タッチ系のイベントとマウス系のイベント両方に対して同じイベントをバインドしているところです。
また、オブジェクトに対してタッチした後、タッチ移動した際にタッチ位置がオブジェクトの外に出た場合、 PCではmouseoutが呼ばれるのに対し、スマートフォンでは対応するイベントがありません。
そこで、挙動を統一する為に、タッチ操作のときのmoveイベント内で、オブジェクトの外に出ていないか判定する処理を設けています。

さらに詳しくは、デモページを用意いたしましたので、併せてご覧ください。
デモページのソースにはコメントも併記しています。

>> demo page
今回のコードを書くにあたり、こちらの記事を参考にさせていただきました!
Developer’s Blog | iPhone/Android/PC 対応。jQuery で書くタッチイベント
以上です!拙い部分もあるやもしれませんが、ご容赦ください。
  • Category: technical / jQuery
  • Posted: 2014/11/28 20:00
  • Author: ユーリ