カテゴリー別アーカイブ: HTML/CSS

[JS/CSS3] JavaScriptによるCSS3の実装判定とトランジション制御

CSSアニメーションで画像を切り替えるjQueryプラグインをちくちくと作り進めていて、現在はとりあえずの試作版が出来たところです。

DEMO

このプラグインの特徴のひとつに「CSS3に対応しない環境では、シンプル(=クロスブラウザ)なトランジションにフォールバックする」というのがあるのですが、これが少しやっかいで、試作版では姑息的な実装でお茶を濁しています。重要な部分なのでしっかりと作っておかないとならないのですが、いかんせんまだ知識に乏しいため、とりあえず現状で分かっている事などについて以下にまとめておこうと思います。

CSS3プロパティ実装の判定方法


前述の特徴を達成するには、まずtransitionやtransformなどといったCSS3プロパティの実装を確かめなければなりません。
そこで、環境に応じてベンダープレフィックスを付与したプロパティ名から機能テスト的に実装を判定するのですが、問題はブラウザやそのバージョンによっては、同じプロパティでも実装の範囲や仕様が異なるということです。(例: transformは2D変形のみ対応 など)

そのため、たとえばtransformの実装を調べる場合、
var supported = 'transform' in document.createElement( 'div' ).style;

といったような、プロパティを持つかどうかのみを判定する方法では、前述の例のような2D変形のみ対応している環境において、’rotateY(100deg)’など3D変形の値が使用された場合に正しく動作しなくなってきます。
なのでそれを回避しようとするなら
var style = document.createElement( 'div' ).style,
    supported = false;
if( 'transform' in style ) {
    style.transform = 'rotateY(100deg)';
    supported = !(style.transform === '');
}

みたいにして実際に値を入れてみる必要があります。
しかしこれだと、2D変形しか使わない場合でも(3D変形に対応していなければ)非対応と見なす処理になるため、それを考慮する場合、

var style = document.createElement( 'div' ).style,
    supported = 0;
if( 'transform' in style ) {
    style.transform = 'rotateY(100deg)';
    supported = style.transform === '' ? 1 : 2;
}

if( supported === 0 ) {
    // 非対応
} else if ( supported === 1 ) {
    // 2Dのみ対応??
} else {
    // 完全対応??
}

みたいにするなどして、プロパティごとにさらに切り分ける必要があります。
しかしこれでもまだやはり100%確実なものではないですし、あくまで「対応していると見なす」という程度でしかありません。

判定するプロパティとその値を限定すればもう少し信頼性を高くできそうですが、制作中のプラグインには「トランジションの種類を選択できる(バージョンアップで追加していく)」という仕様もあり汎用性を必要とするため、それも厳しいところです。

実用性を維持した上で確実に判定することは難しそうなので、色々な環境で調べたりテストしたりしながら、少しずつ理想に近づけていく必要がありそうです。


CSSトランジションの適用


試作版の実装判定とプロパティ名の取得は、前回の記事で作った関数を改修したものを使用しています。これに関しては、最近リリースされたjQuery v1.8.0ではcss()やanimate()にベンダープレフィックスが自動で付与されるようになったのでそれを利用すれば楽そうですが、最新版のjQueryが前提になるのもどうかと思うので、やはり拙くても自力でなんとかしておきたいところです。(といっても試作版はon()とかoff()とか使っているのでv1.7.0以上ありきなんですが)

以上を踏まえて、だいぶ端折ってますがCSSトランジションの適用は現状、以下のようになっています。

~
// 引数に取ったプロパティ名から実装を判定、
// 必要に応じてベンダープレフィックスを付与し文字列を返す
var cssProp = function( name, camelcase ) {
    ...
};

// 引数に取ったプロパティ名から実装を判定、真偽値を返す
// cssProp()のラッパー関数
var checkCSSSupport = function( name ) {
    ...
}
~
// 実装を判定
if ( !checkCSSSupport( ['transform-3d', 'transform-style', 'transition', 'opacity'] ) ) {
    // 非対応ブラウザ用トランジションへフォールバック
    return;
}

// CSSトランジションのコールバックと適用
$( elm ).on( cssProp( 'transitionEnd' ), function(){
    console.log( 'transition finished.' );
});
elm.style[cssProp( 'transform-style' )] = 'preserve-3d';
elm.style[cssProp( 'transition' )] = 'all 1s linear 0s';
// 状況によってうまく動かないのを回避するため遅延して適用
setTimeout(function(){
    elm.style[cssProp( 'transform' )] = 'rotateX(90deg)';
}, 1);
~

かなり泥臭く、そして面倒ですね…。
非対応ブラウザのフォールバックがある以上、CSS3の実装判定はどうしても必要ですが、その適用に関しては判定とは別に考えた方が良さそうです。たとえば以下のように適用できるようjQueryを拡張しておくとか。

~
// 実装の判定
~
// CSSトランジションを適用
$( elm ).nbAnimate({
    transform: 'rotateX(90deg)'
    }, 1000, 'ease-in', function(){
        console.log( 'transition finished' );
    }
});
~

jQuery.cssAnimateとかjQuery.transitとかくらい高い完成度で実現しようとすると大変そうだし荷が重いですが、コードの良し悪しは別として、制作中プラグインの用途に事足りる程度であればなんとかなりそうな気がします。


まとめ


やはりポイントはカオスな実装を如何にスマートかつ確実に判定するか、というところですね。CSS3アニメーションの扱いにくさに関しては、JS側で吸収できる部分が多いので大した問題ではない印象です。面倒臭いですけど。

とりあえずは好奇心が先行しているので気にしていませんが、実際のところこの手のコンテンツって、あくまで一般的な商用サイトにおける使用では、やはり時期尚早だと思いました。
最近の小難しい用語で言う、プログレッシブ・エンハンスメントやポリフィルといった考え方を前提に構成されている物であれば問題無いと思いますが、グレイスフル・デグラデーションみたいな「非対応環境には最低限の演出」という考え方では、クライアントの理解を得るのは中々難しいような気がしますし、第一多くの場合そこまでする必要が無いっていう。

そもそもCSS3自体まだ完成しておらず、実験的に実装されているに過ぎないわけですから、あまり完全を求め過ぎないようにしながら、今後もより良いアプローチを模索していきたいと思います。

この記事の続きはこちら→


[CSS/JS] CSSのプロパティ名をベンダープレフィックス付きで取得

※2012.05.19 : 記事の内容とスクリプトを修正しました。
※2012.08.27 : 以下のコードを改修したものがこちらにあります→


表題のような事がしたかったので、そのような関数を書いてみました。

var cssProp = function( name, camelcase ) {

	var index = (typeof camelcase === 'undefined') ? 0 : camelcase ? 0 : 1,
		cacheObj,
		element,
		prop;

	if ( !cssProp.cache ) {
		cssProp.cache = {
			element: document.createElement( 'div' ),
			vendor: (/webkit/i).test( navigator.appVersion ) ? ['webkit', '-webkit-'] :
				(/firefox/i).test( navigator.userAgent ) ? ['Moz', '-moz-'] :
				(/msie/i).test( navigator.userAgent ) ? ['ms', '-ms-'] :
				'opera' in window ? ['O', '-o-'] :
				'',
			transitionEvent: {
				webkit: 'webkitTransitionEnd',
				Moz: 'transitionend',
				ms: 'MSTransitionEnd',
				O: 'oTransitionEnd'
			}
		}
	}

	cacheObj = cssProp.cache;

	if ( cacheObj.hasOwnProperty( name ) ) {
		return cacheObj[name][index];
	}
	if ( name.toLowerCase() === 'transitionend' ) {
		return cacheObj.transitionEvent[cacheObj.vendor[0]];
	}

	element = cacheObj.element;
	prop = name.replace( /-./g, function( m ) {
		return m.charAt( 1 ).toUpperCase();
	} );

	if ( prop in element.style ) {
		cacheObj[name] = [prop, name];
		return cacheObj[name][index];
	}

	prop = cacheObj.vendor[0] + prop.replace( /./, function( str ) {
		return str.toUpperCase();
	} );

	if ( prop in element.style ) {
		cacheObj[name] = [prop, cacheObj.vendor[1] + name];
		return cacheObj[name][index];
	}

	cacheObj[name] = [undefined, undefined];
	return undefined;
};

サンプル

引数にプロパティ名を渡して呼び出すと、対応している場合は環境に応じたプロパティ名が、対応していない場合はundefinedが返ります。以下はIE9の場合の例。

alert( cssProp( 'transform' ) ); // 'msTransform';
alert( cssProp( 'transition' ) ); // undefined

キャメル記法でなく、ハイフン区切りな文字列で取得したい場合は、第二引数にfalseを渡します。

alert( cssProp( 'transform', false ) ); // '-ms-transform';

ちなみに「3D系のtransformに対応していない場合」とかは考慮してません…(perspectiveの実装を調べればOK?)。あと、transitionEndは実装の判定方法が分からなかったので、そこはすっ飛ばして常にベンダープレフィックスが付与された文字列が返ります。transitionに対応していればイコールtransitionEndも対応とみなして良いものなんでしょーか(‘A’ ?)

とりあえず自分で作ってみたかったというのが第一にあったので挑戦してみましたが、特に理由が無い限りはModernizrというHTML5やCSS3の実装に関する強力なライブラリがあるようなので、そちらを使わせてもらった方がいいですね。

うーん、もっと勉強しないとなー。



[JS/CSS] 背面にある要素のマウスイベントを発火する

ある要素の前面に何か別の要素をかぶせるように配置した時、背面にある要素のマウスイベントは基本的には反応しません。(前面の要素のみ)
当然といえば当然の話なのですが、デザインや仕様によっては、実際に構築するまでこの問題に気付きにくい場合があります。ありました。

下記のサンプルは、透過PNGでできた星画像をリンクボタンにかぶるように position:absolute で配置した例です。見た目の上ではかぶっていませんが、それは星PNGの背景が透過されているからで要素同士はしっかりかぶっているため、背面のリンクボタンにマウスを置いてもカーソルの形は変わりませんし、クリックしても無反応です。(ギリギリかぶっていないリンクボタンの上端と右端は反応する)

サンプル.01

これをどうにか前面の星PNGに邪魔されずにリンクボタンをマウスに反応させるようにする、というのがこの記事の本題。
一番簡単なのは、CSSで

.star { pointer-events: none; }

と書いてその名の通り前面になる要素をマウスに反応させなくする方法なんですが、ブラウザの対応状況から考えてとてもクライアントワークでは使えません。そもそもこのプロパティ自体、独自実装なのかな?

で、どうしたもんかと悩んでいたところで、以下のような記事を見つけました。

– 参考ページ: Forwarding Mouse Events Through Layers

前面要素のハンドラで、自身を一瞬消して(display:none)その間に document.elementFromPoint(x,y) から背面要素を取得する、という流れのようです。多分。
なるほど!という事で、早速泥くさい感じで試してみたのが以下のサンプル。

サンプル.02

とりあえず各主要ブラウザのほか、IE6やiPhoneでも動いているようです。(IE6は透過されませんが)
ただ、上記のサンプルが意図した通り動いているのはたまたまで、 document.elementFromPoint は実装によって挙動が異なるので、実際はもっとちゃんと書かないと破綻してしまう可能性大。iPhoneも拡大したりすると動かない。

– 参考ページ: 要素が画面上に見えているかどうかを調べる – by edvakf in hatena

記事タイトルが示す結果とは違うけど、とりあえず解決のメドがついたのでよしとしたいと思います。もっと根本的な解決方法をご存知の方がいましたら教えて下さいませ。

Flashなら target.mouseEnabled=false で済むから簡単なんだけどなあ…。




[CSS3] CSS3でパノラマVR

一時期によく見かけた気がするパノラマVRをiPhone/iPadに対応できるかみたいなことをふわっと質問され、「Three.jsさんならなんとかしてくれるんじゃないでしょうかー」とよく調べもせずに答えてしまいました。

ちょっと気になったので手持ちのiPhoneで実際に動かしてみたところ、ブラウザで開いた瞬間から明らかにダメそうな空気でしたのですぐにブラウザを閉じました。恐らく実行速度とかそもそもの問題でないかと思います(まあ、これもロクに調べてないんですが)。とりあえずすいませんでした。

まあそのような訳で、以前にウワサでiPhoneではCSS3が安定してるみたいなことを聞いたような記憶があったりなかったりしたので、3D部分はCSS3にやってもらう体で試作してみました。

サンプル(iPhone、iPad、または新しめのSafariのみ)Credit: La caverne aux livres by gadl on Flickr

修正すべき所はいろいろありますが、とりあえずそれっぽいものが出来ました。要素の3D化はCSS3で、マウスドラッグ(フリック)による視点の回転はJS(jQuery)でやってます。

がしかし、そもそもがThree.jsとかPapervision3Dといった3Dエンジンを利用するやり方とは大きく異なるので、仕様によっては実現がかなり難しくなりそう。

とりあえず上記のようにグルグル回すだけのものなら簡単だということは分かったので、今回はここまで。