with the flow

WEBプログラマを目指すWEBデザイナーが書き綴る開発日誌のようなもの

CSSだけでつくる、アニメーションするネオンキューブ

久しぶりに、ブラウザへの実装がだいぶ進んできたCSS3のfilterプロパティを使って、
勉強がてら1作品作ってみることにしました。

できたのがこちら。

See the Pen CSS3 Neon Cubes by hiro (@githiro) on CodePen.

JavaScriptは使わず、動きと回転はanimationプロパティ、
おもちみたいにくっついたり、離れたりする動きはfilterプロパティのcontrast, blur, brightnessを使っています。

基本は、色違いの正方形をクルクル回しながら動かしてるだけです。


blur(ぼかし)をするだけだと、オブジェクト同士が近づいても
ボケの部分が重なるのみ↓ですが、
f:id:mhstid:20141109180959p:plain


contrastを指定することで、blurされた部分が重なった時にスライムみたいにくっつく処理が適用され、
brightnessを指定することで明暗がよりくっきり、重なった部分がオーバーレイ処理されて明るくなっています。

.stage {
  @include filter(blur(50px) brightness(1.4) contrast(40));
}

f:id:mhstid:20141109181012p:plain

応用次第でいろいろな処理が作れそうですね!

SVGでドーナツ型チャートに特化したjQueryプラグインを作った

予告通り、このチャートSVGで書き直しました。

前バージョンとは違って、マウスオーバーするとTooltipが出て数字がわかるようになりました。

作ってみた感想: グラフはSVGで作ったほうがいい。但しcanvasより面倒。

SVGで円弧を描くのはなかなか大変。。。
アンカーポイントの位置を一点ずつ計算して指定してあげないといけないので。
canvasはある程度よろしくやってくれるところがあったんだけどなぁ。。。

そのかわり、mouseenter, mousemoveなどの標準のイベントを監視できるように!
なので、マウスオーバー監視してグラフの値を出すTooltipをつけました。

SVGでやってみて、やっぱりインタラクティブなものはSVGのほうが有利だと実感。
DOMの中にちゃんとパスごとのエレメントが作成されるので操作出来るしなによりデバッグが楽。
CSSである程度装飾も可能だし(box-shadowが使えないのは残念だけど)。
あと、スマホタブレットで拡大しても輪郭がきれい。
canvasだとひと手間加えないといけないし、結局は画像なので細かい部分の小回りが利きにくい。
加工しやすく扱いやすいので好きなんですけどね。Canvas

SVGで円弧を描くのは(ほんとに)大変。

一方SVGはちょっと複雑な図形を書こうとすると途端に難しくなるのが難点。
今回Canvasと同じノリでやろうとしたら、意外と複雑で時間を食った。

Canvasだと円弧を描くのは簡単。
実際に描いてる部分を抜き出すとこんな感じ。

canvas

context.arc(x, y, radius, startAngle, endAngle, anticlockwise)

日本語にすると、

context.arc(中心点x座標, 中心点y座標, 半径, 開始ラジアン, 終了ラジアン, 反時計回りに描画するかどうか)

参考にしたChart.jsでもそうなんですが、ドーナツの一部を切り取った形を作るのに、
単に円弧を描いて線をぶっとくする、といった形ではなく、
円弧の形に一周するようにパスを書いてから中身を塗りつぶすという方式になっています。
該当箇所を抜き出すとこんな感じ。

ctx.beginPath();//描画開始
ctx.arc(centerX, centerY, doughnutRadius, startRadius, startRadius + segmentAngle, false);//外側の線を描く
ctx.arc(centerX, centerY, cutoutRadius, startRadius + segmentAngle, startRadius, true);//内側の線を描く
ctx.closePath();//パスを閉じる

中心2行部分のarc関数を日本語で書き換えるとこうなります。


ctx.arc(中心点X座標, 中心点Y座標, 外側の円弧半径, 開始点ラジアン, 終了点ラジアン, 時計回りに描く);
ctx.arc(中心点X座標, 中心点Y座標, 内側の円弧半径, 終了点ラジアン, 開始点ラジアン, 反時計回りに描く);

2行目の最後が「true」、つまり反時計周りになっているのは、そうしないと円弧の周りをパスが一周しないから。
外側の線を描いた後、内側の線を描くわけなんですが、外と内をつなぐ部分の線を描かなくても、
自動的につないでくれるのがCanvasのよいところ。

一方SVGは…

SVG

SVGでパスを描く構文は複雑なので細かい説明は省きますが、こんな感じ。

var cmd = [
  'M', startX, startY,
  'A', doughnutRadius, doughnutRadius, 0, largeArc, 1, endX, endY,
  'L', startX2, startY2,
  'A', cutoutRadius, cutoutRadius, 0, largeArc, 0, endX2, endY2,
  'Z'
];
$paths[i][0].setAttribute("d",cmd.join(' '));

先頭のM,A,L,A,Zって部分でどんなパスを描くかを宣言しています。日本語だとこう。

'M'(線は描かずペン先を移動) 開始点X座標, 開始点Y座標
'A'(円弧を描く), 外側の円弧半径(X方向), 外側の円弧半径(Y方向), 回転角(度), 長弧or短弧, 時計周り, 終点X座標, 終点Y座標
'L'(線を描く), 円弧終点の内側の点X座標, 円弧終点の内側の点Y座標
'A'(円弧を描く), 内側の円弧半径(X方向), 内側の円弧半径(Y方向), 回転角(度), 長弧or短弧, 反時計周り, 終点X座標, 終点Y座標
'Z'(パスを閉じる)

はい、複雑すぎ。

複雑になっちゃうのは、開始点と終了点のX,Y座標を
いちいち計算して出してあげないといけないところから来てると思います。
Canvasだと、半径と角度を指定すれば自動計算してくれるのに。。。
今回はまともに向き合ったのでいろいろ勉強しなおすことに。

ともかくこんな感じでチクチク点を指定して描くことでやっと完成。根本的に理解するためには三角関数からやり直さないといけなかった。

一応参考にしたリンクを貼っときます。

・10分でわかるSVG 基礎編
http://www.atmarkit.co.jp/fwcr/design/benkyo/webgraphics02/02.html

・中学生でもわかるベジェ曲線
http://ruiueyama.tumblr.com/post/11197882224

・三角比・三角関数
http://www24.atpages.jp/venvenkazuya/math1/trigonometric_ratio1.php

・360度ではなく、ラジアンで角度を記述する意味は?
http://detail.chiebukuro.yahoo.co.jp/qa/question_detail/q1211672837

Canvasでドーナツ型チャートに特化したjQueryプラグイン作った

ドーナツ型チャートに特化したjQueryプラグインを試しに組んでみました。


Chart.jsのソースを大いに参考にさせてもらいました。
(Chart.jsは読みやすくてお勧めです)

・初期段階でドーナツ状のグラフが通るレールを予め描画するようにした。
・色つきグラフ部分は初めopacity=0で、ラジアンが2π(終点)に近づくに従ってopacity=1になるようにした。
・shadowなど細かいデザインの作りこみが出来るようにした。
・中心にアニメーションするテキストを出すようにした。テキストを出すロジックはまだ微妙。
・各色のlabelはhtml+cssで出す事前提のスクリプトです。

Chart.js(MIT License)
http://www.chartjs.org/

※各項目の所に出すlabelはhtml+cssだけで作るのが現実的だけど、
せっかくだからマウスオーバーで出すとかしたい・・・
でもcanvasでは色々と面倒なので、
何かこれ自体SVGで書いちゃった方がいい気がしてきた。
ということで次回はSVGで書きだすバージョンを検討中。。。

#追記:SVGバージョン書きました。
http://me.hateblo.jp/entry/2013/11/27/215543

マウスオーバー時にアニメーションするボタン Part2 作った

CSSだけで頑張るシリーズ、アニメーションボタン編 Part2

http://www.oregongridiron.com/ ←このサイトのボタンがかっこよかったので
どうやってるか調べて自分なりに実装しなおしてみた。

かっこいいエフェクトができるけど、html側で必要なタグが多い。。。

マウスオーバー時にアニメーションするボタン Part1 作った

CSSだけで頑張るシリーズ、
アニメーションするボタンに挑戦中。
マウスオーバーでアニメーションします。
ボタンだけだとつまんないのでぼかした画像+図形パターンという最近の
流行デザインの背景もつけてみた。

transformだけでいろんな表現ができることに驚き。
シリーズ化するかもしれないので「Part1」にしときます。

Canvasでjpgを透過PNGに変換するjQueryプラグイン作った

背景を透けて見せる等ができる透過PNGを、canvasで作成できる(グラデーションアルファマスクを作る)jQueryプラグイン作った。

f:id:mhstid:20131105125224j:plain

透過PNGRGB+Alphaチャンネルなので、今回みたいに写真の透過PNGだと
普通にサイズが1M近くになっちゃう。でもデザインの都合上透過PNGでなきゃいけない場合もあるわけで。。。

そこで、JPG画像とマスク用の画像を元にして
canvasで透過PNGに加工してしまい、サイズを節約するプラギン作りました。
要はPhotoShopで普段やる「透過PNG書き出し」をcanvasでやってしまおうというもの。

→これでサイズが1/5位にできた!調べたらAppleのサイトとかでもフツーに使われてるテクニックだった。
ということでおそらくもっと優秀なプラギンは山のようにあると思います。

あと一応、画像がそれぞれ呼ばれた後と、すべての画像加工が終わった後にcallbackできるようにしています。
今回使ってないけど。詳しくはソースを。

苦労したのはマスクを掛けるjpeg画像とマスク元画像が完全にloadされてからcanvasでdrawしないと
どちらかしか・またはまったく描画されないこと。
特にIEではキャッシュ画像を読んだ場合はonload自体が発生しないことがある。IEまたおまえか。。。
ということで、BuggyなIE9の場合は、画像を呼び出すときに末尾にクエリストリングをつけるという苦肉の策で対応。

使い方。

■HTML側
→透過PNG生成に必要な画像のパスは、すべてhtml側にdata属性で書き出しておく。
srcに写真のURL書いたらリクエストが飛んじゃって、読み込みサイズを小さくするという
プラグインの目的が果たせなくなるので、透過gif画像にしています。

※どうしてもSEO的にやりたくない場合はdisplay: noneか、こういうテクニックを利用して見えなくしたaタグにimgへのリンク貼るとかしか思いつかないです。

<img src="blank.gif"  data-origsrc="original.jpg" data-jpegsrc="image.jpg" data-filtersrc="filter.png" width="200" height="200" alt="image alt">

"data-origsrc"にはcanvas非対応ブラウザ用に透過PNG版の画像パス、
"data-jpegsrc"にはアルファマスクしたいjpeg画像(png,gifでも可)、
"data-filtersrc"にはアルファマスクをするpng画像(透明にしたい部分を黒にした透過png)を指定。

■JS側
onready以降の好きなタイミングでcreateAlphaJpegを実行するだけ。

$(function() {
  $("img").createAlphaJpeg();
  $("#imgWrapper").createAlphaJpeg();//(←画像を含むwrapperでも可能)
});

optionsも指定できますが詳しくはソースを見てください。
以下のURLを参考にしたけど、まだバグがあるかも。。。

参考にしたプラグインスニペット
https://gist.github.com/yemster/1539102
https://github.com/7vsy/ImgLoader/blob/master/ImgLoader.js

一応作ったJSも貼っておきます。

(function($, undefined) {
	$.fn.createAlphaJpeg = function(options) {
		var $this = this,
			$images = $this.find('img').add($this.filter('img')),//thisがimg自体でもimgを含むdivなどでも、すべて検索してとにかくimgを拾ってくる
			len = $images.length,
			blankGif = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==',
			options = $.extend({
				afterEachImageDrawed: function(){  },
				afterAllImagesDrawed: function(){  },
				origSrc: "origsrc",
				filterSrc: "filtersrc",
				jpegSrc: "jpegsrc",
				bugBrowser: ['msie 9'],
				alphaJpegClass : 'alphaJpeg'
			}, options),
			canUseCachedImages = (function() {// IE9がキャッシュ画像を使ったときにloadが発生しないことがある為対策
				var ua = window.navigator.userAgent.toLowerCase();
				var m = ua.match(/msie ([0-9]*)/);
                if (m != null && $.inArray(m[0], options.bugBrowser) !== -1) {
		    		return false;
			    }else{
			    	return true;
			    }
			})(),
			isCanvasSupported = (function() {
				var elem = document.createElement('canvas');
				return !!(elem.getContext && elem.getContext('2d'));
			})(),
			$currentImage = [],
			filterImg = [],
			jpegImage = [],
			$canvasImage = [],
			drawedImagesCnt = 0;

		// functions
		function loadFilter(id) {
			filterImg[id] = new Image();
			filterImg[id].onload = function(){ createJpeg(id) };
			var src = $currentImage[id].attr("data-" + options.filterSrc);
			src = (canUseCachedImages)? src : src + "?" + new Date().getTime();
			filterImg[id].src = blankGif;
			filterImg[id].src = src;
		}
		function createJpeg(id) {
			jpegImage[id] = new Image();
			jpegImage[id].onload = function(){ createCanvas(id) };
			var src = $currentImage[id].attr("data-" + options.jpegSrc);
			src = (canUseCachedImages)? src : src + "?" + new Date().getTime();
			jpegImage[id].src = blankGif;//webkit hack from https://groups.google.com/forum/#!topic/jquery-dev/7uarey2lDh8
			jpegImage[id].src = src;
		}
		function createCanvas(id) {
			var w = $currentImage[id].width();
			var h = $currentImage[id].height();
			$canvasImage[id] = $('<canvas class="' + options.alphaJpegClass + '" width="' + w + '" height="' + h + '"></canvas>');
			$currentImage[id].replaceWith($canvasImage[id]);
			var ctx = $canvasImage[id][0].getContext('2d');
			ctx.drawImage(jpegImage[id], 0, 0, w, h);
			ctx.globalCompositeOperation = 'xor';//重なり部分を除外モードにする
			ctx.drawImage(filterImg[id], 0, 0, w, h);
			drawedImagesCnt++;
			options.afterEachImageDrawed.call($this, $canvasImage);
			if (drawedImagesCnt === len) options.afterAllImagesDrawed.call($this, $canvasImage);
		}
		for (var i = 0; i < len; i++) {
			$currentImage[i] = $images.eq(i);
    		if ($currentImage[i].attr("data-" + options.origSrc) == null) break;
			if (!isCanvasSupported) {//canvas非サポートの場合はJPEGをそのまま書き出す
				$currentImage[i].attr("src", $currentImage[i].attr("data-" + options.origSrc));
			} else {
				loadFilter(i);
			}
		}
		return $this;
	};
})(jQuery);

$(function()
{
	$("img").createAlphaJpeg();
});

※MITなので使うのは自由ですが当方で責任は一切持ちません。

Sass(Compass)でRetina用backgroundを同時に書き出すmixinを作った

Retina(に限らず高解像度)用の画像を背景画像として書き出すとき、
今までは全部手作業でやってたんだけども、

  • 高解像度向けの@mediaのベストプラクティスが日々変化(進化)するので、毎回grepして修正するのが面倒。
  • @mediaの記述位置はCSSの末尾にまとめて書いてたが、数が多いとどこに対応してるか、訳が分からなくなってきて保守地獄発生。かといって対応セレクタの直後に書くとなんか見通しが悪くなるのでこれも避けたい。
  • そもそもbackground-image: url("path/to/image/hoge@2x.png);とかbackground-sizeとか毎回書くのが大変。なんでRetinaのためにコード書く量が2倍以上になるのか。。。

→そこでsvgですよ、という話もあるんだけど、IE8以下対応してないし、画像でしか表現できないものもあるわけで。


こんな時、Sassでmixin定義したら上の問題が一気に解決できたので共有。
※Sass(もしくはSCSS,Compass)がどんなものか、どれほど便利かは、優れた解説記事がこちらこちらで見られます。

具体的には、こんなmixinを書きます。

SASS

@mixin bgretina($bgwidth, $bgHeight, $bgCol, $image, $extension, $posX:0, $posY:0, $repeat:no-repeat, $ratio:'@2x') {
	background: $bgCol url($image + '.' + $extension) $posX $posY $repeat;
	@media only screen and (-webkit-min-device-pixel-ratio: 1.5),
		   only screen and (min-resolution: 192dpi) {
				background-image: url($image + $ratio + '.' + $extension);
				@include background-size($bgwidth $bgHeight);
	}
}

使い方はこんな感じ。

SASS

.hoge {
	@include bgretina(50px, 50px, #fff, path/to/image/hoge, png);
}

上のように、1行sass(scss)ファイルに書いてコンパイルすると、CSSには以下が自動的に書き出される!

CSS

.hoge {
	background: #fff url(path/to/image/hoge.png) 0 0 no-repeat;
}
@media only screen and (-webkit-min-device-pixel-ratio: 1.3), only screen and (min-resolution: 192dpi) {
	.hoge {
		backgrond-image: url(path/to/image/hoge@2x.png);
		-webkit-background-size: 50px 50px;
		-moz-background-size: 50px 50px;
		-o-background-size: 50px 50px;
		background-size: 50px 50px;
	}
}

※background-sizeにCompassのmixin使ってるのは、"-webkit-background-size"を書かないとAndroid2.1-2.3でサイズが変更されないから。
あと、caniuseみると、一応-moz-や-o-必要なバージョンがあったみたいなので。
IEいきなり9から対応しているのでprefixいらず。
http://caniuse.com/#search=background-size

background-positionとbackgroun-repeat,画像名の後に付ける"@2x"とかの変更にも対応しています。
引数増えるけど、こんな感じ。

SASS

.hoge {
	@include bgretina(50px, 50px, #fff, path/to/image/hoge, png, 0, 50%, repeat-y, @3x);
}


CSS

.hoge {
	background: #fff url(path/to/image/hoge.png) 0 50% repeat-y;
}
@media only screen and (-webkit-min-device-pixel-ratio: 1.3), only screen and (min-resolution: 192dpi) {
	.hoge {
		backgrond-image: url(path/to/image/hoge@3x.png);
		-webkit-background-size: 50px 50px;
		-moz-background-size: 50px 50px;
		-o-background-size: 50px 50px;
		background-size: 50px 50px;
	}
}

  • @mediaのベストプラクティスが変わったら、mixinだけを書き換えて再コンパイルすれば全ファイル一括書き換えもカンタン。
  • 保守するのはscssファイルだけでいいので、@mediaの位置が離れて見づらいとかの問題も解決!
  • なによりたった1行書くだけで、今まで10行書いてた苦痛から解放される!

本当は引数の$imageと$extensionは、分けずにmixin側で分割したい
(@include bgretina(50px, 50px, #fff, path/to/image/hoge.png);って書きたい)んですが、
文字列操作とかはSass3.3以降でしかサポートされないみたいですね。
安定版としてリリースされたらmixin書き直すかも。。