日記
メディアクエリーと相性がよさそうな CSS 小技いくつか
新しい技術というと CSS3 のプロパティーの活用に目が行きがちですが、CSS2 でこれまで使う機会がなかった display:table 系の表示に注目してみるとかなり面白いことができます。table 系の表示は、IE7 までサポートされてきませんでしたが、IE8 からサポートされています。メディアクエリーはというと、IE9 以降ですので、メディアクエリーを採用した Web ページの構築とかなり相性がいいと言えるでしょう。
| IE | 9+ |
|---|---|
| FIrefox | 3.5+ |
| Chrome | 4 (or before) + |
| Safari | 3.1+ |
| Opera | 9.5+ |
table 系の表示にあまり注目されることがないように感じますので、これまで仕事でメディアクエリーを利用してくる中で考えた小技をこの記事に共有します。ここに載せているのは一応実際につかって検証済みですが、もし変な動作があったら教えてください。
上下を入れ替える
display プロパティーの table-row-group や table-header-group などの値を応用すれば、上下の入れ替えが可能です。説明だと分かりづらいので、demo をご参照ください。
この方法は、対象デバイスによって、コンテンツのプライオリティーが変わる場合に有効でしょう。また、table-footer-group をさらに加えれば、上、中、下の 3つを自由に入れ替えることもできます。
なお、これをさらに使いやすくしたような仕組みの CSS Grid Layout モジュールが CSS3 として策定中で、IE10 では利用することができます。
フレキシブルな横並び
フレキシブルな横並びというと、CSS3 の Flexible Box を思い浮かべる人も多いかもしれませんが、IE 9 では Flexible Box は利用できません。そのかわり、table 系の表示を利用すればそれに近いことができます。説明だと分かりづらいので、demo をご参照ください。
table-layout:fixed; を宣言すれば、均等の幅になりますし、これを外せば内容に合わせた横幅になります。
表組みを崩す
ここまで挙げた2つの例は、block の表示から table の表示にするという例でしたが、その逆も可能です。例えば、表組みは小さい画面だとかえって見づらいものです。そこで、小さい画面の場合には、table 表示を block 表示へと変えてあげることもできます。実際の動作は demo をご参照ください。
ただ、各ブラウザーにおいて少しだけ注意点があり、少し回りくどい方法で行わなければいけません
Webkit 向け
表組み関連の要素を全て block 表示にしておかないと、意図しない動作が起こります。忘れがちになりますが、tbody や tr も併せて block 表示にしておきます。
Firefox 向け
col 要素で幅を決めていると、block にした後もその幅が維持されてしまいます。ですので col 関連の要素の表示を消しておきます。
IE 向け
display:block; にしても表組みの表示を変更することができませんが、float :left などで強制的に block にしてやることで表組みの表示を崩すことができます。併せて width:100%; もしれておきます。padding を使えるように、box-sizing も加えておきます。
おまけ 1 : リンク部分を大きくする
メディアクエリーというと、大胆な変形を想像されるかもしれませんが、変形せずとも、指での操作を想定して、リンク部分だけ大きくしてあげる、といった気配りも大切です。こういった気配りだけでも十分にメディアクエリーを利用する価値があるでしょう。
おまけ 2 : 画像をフレキシブルにする
画像は width:100; height:auto; にするとグネグネと伸縮するようになります。これにより、大きな画像でも、デバイスの幅にフィットさせることができます。
さいごに
CSS2 でも色々できます。ここでは挙げませんでしたが、セレクターや before,after擬似要素も CSS2 です。そしてこれらの多くは IE8 でも使えます。CSS2 も研究してみるのと未だに面白いです。
あ、あと、メディアクエリーを利用するときには <meta name="viewport" content="width=device-width, initial-scale=1.0"> も忘れずに宣言しておきましょう
detecting click event in THREE.js
WebGL は、2D canvas と同様に 1つの canvas 要素の中に描画されるため、描画された 3D オブジェクトに対して、マウスイベント等を判定するのは少し大変そうに思えます。
しかし、THREE.js には Ray 判定の機能を提供しており、Ray によりクリックやマウスオーバーなどのイベントを取得することができます。
Ray というのは、3D のプログラムでは結構知られている方法で、例えば DirectX 用の資料にも同様の方法の解説があります。
Ray の仕組みは図にすると以下のようになります。画面上では 2D 空間のため、x, y だけの世界です。

Ray の考え方は x, y の 2D の点を、near 面から far 面まで伸びる線として扱います。そしてこの線に 3D オブジェクトが接したかどうかを判断します。

THREE.js では、次のような手順で Ray を利用します。
1. 判定するメッシュの集合を決める
判定したい mesh を meshArray などのような配列に入れる。scene 内の全ての 3D オブジェクトを判定するなら特にここで配列を用意せず、あとで scene マルごとを利用することも可能
2. Projector を用意
new THREE.Projector();
3. カーソル位置をとって 3次元ベクトルに変換する
var x = (mouseX / renderer.domElement.width) * 2 - 1;
var y = - (mouseY / renderer.domElement.height) * 2 + 1;
var vector = new THREE.Vector3(x, y, 1);
4. Projector にカーソル位置のベクトルとカメラ位置を渡す
projector.unprojectVector(vector, camera);
5. カーソル位置の3次元ベクトルとカメラの位置を Ray に渡す
var ray = new THREE.Ray(camera.position, vector.subSelf(camera.position).normalize());
6. イベントで判定された 3D オブジェクトを取り出す
ray.intersectObjects(meshArray) あるいは ray.intersectScene( scene ) に判定の結果、Ray と交差した全ての Mesh が手前から順番に入っている配列を取得できるのでそれをいろいろできる。判定を貫通させたくなければ、配列の [0] 番目のみ操作すればいい
例えば次のようなコードを書けばOK : detecting click event in THREE.js
window.addEventListener('DOMContentLoaded', (function(){
var width = 600;
var height = 400;
//シーン
var scene = new THREE.Scene();
//カメラ
var camera = new THREE.PerspectiveCamera( 40, width / height, 1, 1000 );
camera.position.x = 10;
camera.position.z = 20;
scene.add( camera );
//ライティング
var light = new THREE.DirectionalLight( 0xffffff, 1.5 );
light.position.set( 1, 1, 1 ).normalize();
scene.add( light );
var light2 = new THREE.DirectionalLight( 0xffffff );
light2.position.set( -1, -1, -1 ).normalize();
scene.add( light2 );
//レンダラー
renderer = new THREE.WebGLRenderer();
renderer.setSize( width, height );
//レンダラーを append
document.body.appendChild( renderer.domElement );
var meshArray = [];
var geometry = new THREE.CubeGeometry(2, 2, 2, 6, 6, 6);
for(var i = 0; i < 5; i++){
meshArray[i] = new THREE.Mesh( geometry, new THREE.MeshLambertMaterial( { color: Math.random() * 0xffffff } ) );
meshArray[i].position.x = 2 * i;
meshArray[i].position.z = -i;
scene.add( meshArray[i] );
}
var theta = 0;
(function(){
renderer.render( scene, camera );
camera.position.x = 20 * Math.sin( theta * Math.PI / 360 );
camera.position.y = 20 * Math.sin( theta * Math.PI / 360 );
camera.position.z = 20 * Math.cos( theta * Math.PI / 360 );
camera.lookAt( scene.position );
theta++;
setTimeout(arguments.callee, 1000 / 32);
})();
// 任意の要素のオフセットを取得用関数 (あとで canvas のオフセット位置取得用)
var getElementPosition = function(element) {
var top = left = 0;
do {
top += element.offsetTop || 0;
left += element.offsetLeft || 0;
element = element.offsetParent;
}
while (element);
return {top: top, left: left};
}
//Ray 関連一式 たぶんこれ使いまわせば何でも行けると思う。
var projector = new THREE.Projector();
renderer.domElement.addEventListener('click', function(e){
var mouseX = e.clientX - getElementPosition(renderer.domElement).left;
var mouseY = e.clientY - getElementPosition(renderer.domElement).top;
var x = (mouseX / renderer.domElement.width) * 2 - 1;
var y = - (mouseY / renderer.domElement.height) * 2 + 1;
var vector = new THREE.Vector3(x, y, 1);
projector.unprojectVector(vector, camera);
var ray = new THREE.Ray(camera.position, vector.subSelf(camera.position).normalize());
var intersects = ray.intersectObjects(meshArray);
if(intersects.length > 0){
console.log(intersects[ 0 ].object);
var color = Math.random() * 0xffffff;
intersects[ 0 ].object.material.color.setHex( color );
}
renderer.render( scene, camera );
}, false);
}), false);
New 3D physics engine – cannon.js
最近、3D の物理演算エンジンとして cannon.js 作ってるよ という方が現れました。
3D の物理演算エンジンとしてよく知られているライブラリーに、ammo.js や jiglib.js があります。そして、どちらかと言うと ammo.js が有名なようですが、元が bullet という C++ のライブラリーからの移植というだけあってかなかなかクセがあってめんどくさいです。
一方で、cannon.js は THREE.js のような扱いやすさと ammo.js のような物理演算を提供してくれます。現在はまだできることが少ないのですが THREE.js とセットでいい感じに育ってくれるといいなぁと思いつつ。
cannon.js と THREE.js を組み合わせると次のような物理演算が可能です : 球体と平面衝突の demo
cannon.js の利用方法は THREE.js に近く、次のように書いていきます。
1 初期設定など
var world = new CANNON.World();
world.gravity(new CANNON.Vec3(0 ,0, -50));
var bp = new CANNON.NaiveBroadphase();
world.broadphase(bp);
world.iterations(2);
読んだままのごとく…。broadphase というのは、ぶつかりそうなオブジェクトどうしにまとめて全部処理しないで済むようするための集合みたい
2 あとで使う配列の用意
var phys_bodies = [];
var phys_visuals = [];
後で使うための配列を用意しておきます
phys_bodies: 剛体化した cannon のオブジェクト格納用phys_visuals: 剛体化した 3D オブジェクト格納用 (THREE を併用しているならその Mesh)
3. 平面 (地面) の剛体をつくる
var groundShape = new CANNON.Plane(new CANNON.Vec3(0, 0, 1));
var groundBody = new CANNON.RigidBody(0, groundShape);
world.add(groundBody);
CANNON.Plane で平面を用意し、CANNON.RigidBody で剛体化し、cannon の world に add します
4. 球体の剛体を作る
var sphereShape = new CANNON.Sphere(1);
var sphereBody = new CANNON.RigidBody(5,sphereShape);
半径を決めてCANNON.Sphereで球体を作ります(上記は半径1)。そして剛体化します。このとき、THREE.js などの見た目側の半径と一致するようにしておきます。例えば new THREE.SphereGeometry( 1, 8, 8); みたいに。
4.2. 球体の初期位置を決める
var pos = new CANNON.Vec3(0, 0, i * 4 + 4);
sphereBody.setPosition(pos.x + randX, pos.y + randY, pos.z);
3 次元ベクトルにして剛体化した cannon の球体に渡します
4.3. world に球体情報を追加
phys_bodies.push(sphereBody);
phys_visuals.push(spheremesh);
world.add(sphereBody);
のように cannon の world に add します。ついでに一番最初に用意した配列に格納しておきます。
5. アニメーションさせる
function updatePhysics(){
// Step world
if(!world.paused){
world.step(1.0 / 60.0);
for(var i = 0, l = phys_bodies.length; i < l; i++){
phys_bodies[i].getPosition(phys_visuals[i].position);
phys_bodies[i].getOrientation(phys_visuals[i].quaternion);
}
}
}
連続レンダリングする際に、次のような関数で見た目オブジェクトと、cannon の位置情報を紐付けます
まとめ
THREE.js を併用している場合は、次のようになります。すごくわかりやすい! 実際に動く demo も用意してあります。ただ、2012 年4 月 2 日の段階では box 同士の衝突などはまだ解決されてないみたいです。でも今後にすごく期待。
<script src="Three_r48.js"></script>
<script src="cannon.min.js"></script>
<script>
window.addEventListener('DOMContentLoaded', function(){
// Physics
var world = new CANNON.World();
world.gravity(new CANNON.Vec3(0 ,0, -50));
var bp = new CANNON.NaiveBroadphase();
world.broadphase(bp);
world.iterations(2);
var phys_bodies = [];
var phys_visuals = [];
var width = 1000;
var height = 400;
init();
animate();
function init() {
// scene
scene = new THREE.Scene();
// vamera
var near = 1;
var far = 1000;
camera = new THREE.PerspectiveCamera( 40, width / height, near, far );
camera.position.x = 0;
camera.position.y = -20;
camera.position.z = 10;
scene.add( camera );
// light
var light = new THREE.DirectionalLight( 0xffffff, 1.5 );
light.position.set(1, -1, 1 ).normalize();
scene.add( light );
// renderer
renderer = new THREE.WebGLRenderer();
renderer.setSize( width, height );
document.body.appendChild( renderer.domElement );
createScene();
}
function createScene( ) {
// ground
var geometry = new THREE.PlaneGeometry( 20, 20 );
var planeMaterial = new THREE.MeshBasicMaterial( { color: 0xeeeeee } );
var ground = new THREE.Mesh( geometry, planeMaterial );
scene.add( ground );
// ground plane
var groundShape = new CANNON.Plane(new CANNON.Vec3(0, 0, 1));
var groundBody = new CANNON.RigidBody(0, groundShape);
world.add(groundBody);
var sphere_geometry = new THREE.SphereGeometry( 1, 8, 8); //SphereGeometry( radius, segments, rings)
// Sphere on plane
var sphereShape = new CANNON.Sphere(1); // Sharing shape saves memory
for(var i = 0; i < 10; i++){
var sphereMaterial = new THREE.MeshLambertMaterial( { color: Math.random() * 0xffffff } );
var spheremesh = new THREE.Mesh( sphere_geometry, sphereMaterial );
scene.add( spheremesh );
spheremesh.useQuaternion = true;
// Physics
var randX = (Math.round(Math.random() * 9) + 1 - 5) * .2;
var randY = (Math.round(Math.random() * 9) + 1 - 5) * .2;
var sphereBody = new CANNON.RigidBody(5,sphereShape);
// start pos
var pos = new CANNON.Vec3(0, 0, i * 4 + 4);
sphereBody.setPosition(pos.x + randX, pos.y + randY, pos.z);
// Save initial positions for later
phys_bodies.push(sphereBody);
phys_visuals.push(spheremesh);
world.add(sphereBody);
}
}
function animate(){
requestAnimationFrame( animate );
updatePhysics();
render();
}
function updatePhysics(){
// Step world
if(!world.paused){
world.step(1.0 / 60.0);
for(var i = 0, l = phys_bodies.length; i < l; i++){
phys_bodies[i].getPosition(phys_visuals[i].position);
phys_bodies[i].getOrientation(phys_visuals[i].quaternion);
}
}
}
function render(){
camera.lookAt( scene.position );
renderer.clear();
renderer.render( scene, camera );
}
}, false);
</script>
SVG Gradient をSASS (LESS) で生成して CSS Gradient として扱う mixin
この記事では最終的に、linear-gradient 関数を SASS の mixin にまとめたコードを解説します。これは、Webkit 系ブラウザーや Firefox だけでなく Internet Explorer 9 でも SVG 経由で再現可能でとても便利です。
まず、利用例を紹介しますと、次のように mixin を利用することになります。
.selector{
@include linear-gradient("to bottom", "0FF", 0, "00F", 20, "0F0", 100);
}
それぞれの引数は、<方向>,<色1>,<位置1>,<色2>,<位置2>,<色3>,<位置3>…の順に指定しています。この時、諸事情により
- <方向>に指定できるのは
[to left | to right | to top | to bottom]の 4つのうちのいずれかだけにしています(策定中の仕様がいろいろ変わっていて大変なので…)。 - 色には
#は付けない。また、16進数のみ - 位置にはパーセンテージで渡す。単位は付けない
ようにしています。
そして、これをコンパイルすると次の CSS のコードが出力されます。
.selector {
-pie-background: linear-gradient(top, #0FF 0%,#00F 20%,#0F0 100%);
background-image: url(data:image/svg+xml,%3c%3fxml%20version%3d%221%2e0%22%3f%3e%3csvg%20xmlns%3d%22http%3a%2f%2fwww%2ew3%2eorg%2f2000%2fsvg%22%20width%3d%22100%25%22%20height%3d%22100%25%22%3e%3cdefs%3e%3clinearGradient%20id%3d%22G%22%20x2%3d%220%25%22%20y2%3d%22100%25%22%3e%3cstop%20style%3d%22stop%2dcolor%3a%230FF%22%20offset%3d%220%25%22%2f%3e%3cstop%20style%3d%22stop%2dcolor%3a%2300F%22%20offset%3d%2220%25%22%2f%3e%3cstop%20style%3d%22stop%2dcolor%3a%230F0%22%20offset%3d%22100%25%22%2f%3e%3c%2flinearGradient%3e%3c%2fdefs%3e%3crect%20width%3d%22100%25%22%20height%3d%22100%25%22%20fill%3d%22url%28%23G%29%22%2f%3e%3c%2fsvg%3e);
background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #0FF),color-stop(20%, #00F),color-stop(100%, #0F0));
background-image: -webkit-linear-gradient(top, #0FF 0%,#00F 20%,#0F0 100%);
background-image: -moz-linear-gradient(top, #0FF 0%,#00F 20%,#0F0 100%);
background-image: -ms-linear-gradient(top, #0FF 0%,#00F 20%,#0F0 100%);
background-image: -o-linear-gradient(top, #0FF 0%,#00F 20%,#0F0 100%);
background-image: linear-gradient(to bottom, #0FF 0%,#00F 20%,#0F0 100%); }
なお、上記ソースコード内の dataURI は Internet Explorer 9 で linear-gradient とほぼ同じことをするための記述です。
では、この mixin の中身はどうなっているかを、順を追って解説します。この仕組みをそのまま利用すれば LESS 用の mixin へのコンバートも簡単です。
CSS の gradient 関数を利用すれば画像生成をすることができます。線形グラデーションを利用する際には、-接頭辞-linear-gradient() と古い Webkit の記法を併せて記述することが多いのではないでしょうか。
例えば次のように書くことになるでしょう。なお、この記事執筆現在での最新の linear-gradient の方向の指定は、top などから、to bottom などに変更になっています。
.selector {
background-image: -webkit-linear-gradient(top, #0FF 0%,#00F 20%,#0F0 100%);
background-image: -moz-linear-gradient(top, #0FF 0%,#00F 20%,#0F0 100%);
background-image: -ms-linear-gradient(top, #0FF 0%,#00F 20%,#0F0 100%);
background-image: -o-linear-gradient(top, #0FF 0%,#00F 20%,#0F0 100%);
background-image: linear-gradient(to bottom, #0FF 0%,#00F 20%,#0F0 100%); }
しかし、Internet Explorer 9 では CSS の gradient 関数には対応していません。(Internet Explorer 10 から対応される予定です)
そこで Internet Explorer 9 には SVG を併せて利用する方法が知られています。
.selector {
background-image: url(gradient.svg);
background-image: -webkit-linear-gradient(top, #0FF 0%,#00F 20%,#0F0 100%);
background-image: -moz-linear-gradient(top, #0FF 0%,#00F 20%,#0F0 100%);
background-image: -ms-linear-gradient(top, #0FF 0%,#00F 20%,#0F0 100%);
background-image: -o-linear-gradient(top, #0FF 0%,#00F 20%,#0F0 100%);
background-image: linear-gradient(to bottom, #0FF 0%,#00F 20%,#0F0 100%); }
上記で読み込まれている gradient.svg の内容は、次のとおりです。
<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%">
<defs>
<linearGradient id="G" x2="0" y1="100%">
<stop style="stop-color:#0FF" offset="0%"/>
<stop style="stop-color:#00F" offset="20%"/>
<stop style="stop-color:#0F0" offset="100%"/>
</linearGradient>
</defs>
<rect width="100%" height="100%" fill="url(#G)"/>
</svg>
CSS の gradient 関数は、SVG に由来していることもあり、コードを見比べてみればどこが何を表しているのかはすぐにわかるのではないでしょうか。
しかし、外部ファイルにしてしまうと CSS における gradient 関数のような利便性は失われてしまいます。そこで dataURI を利用します。dataURI は、バイナリーデータを base64 にエンコードして使うことが多いのですが、SVG の中身はバイナリーではなく XML でありテキストデータです。ですので、base64 エンコードせずとも、URL エンコードしてやれば dataURI として利用することができます。
先程の SVG を URL エンコードすると次のようになります。
%3c%3fxml%20version%3d%221%2e0%22%3f%3e%3csvg%20xmlns%3d%22http%3a%2f%2fwww%2ew3%2eorg%2f2000%2fsvg%22%20width%3d%22100%25%22%20height%3d%22100%25%22%3e%3cdefs%3e%3clinearGradient%20id%3d%22G%22%20x2%3d%220%25%22%20y2%3d%22100%25%22%3e%3cstop%20style%3d%22stop%2dcolor%3a%230FF%22%20offset%3d%220%25%22%2f%3e%3cstop%20style%3d%22stop%2dcolor%3a%2300F%22%20offset%3d%2220%25%22%2f%3e%3cstop%20style%3d%22stop%2dcolor%3a%230F0%22%20offset%3d%22100%25%22%2f%3e%3c%2flinearGradient%3e%3c%2fdefs%3e%3crect%20width%3d%22100%25%22%20height%3d%22100%25%22%20fill%3d%22url%28%23G%29%22%2f%3e%3c%2fsvg%3e
<、>、スペースが %3c, %20, %3e などに置き換わっているだけです。ですので、URL エンコードは、base64 エンコードと違い、ある程度内容を読むことができます。これならば、SASS の mixin に利用できるわけです。
上記の URL エンコード化した SVG の中を見てみると、途中に色(0FFなど)やオフセット(0など)がそのまま入っていることがわかります。抜粋すると例えば次などです。
%3cstop%20style%3d%22stop%2dcolor%3a%230FF%22%20offset%3d%220%25%22%2f%3e
そして、ここに mixin の引数として入力した値をそのまま入れてやればいいのです。以上の結果から冒頭で紹介した mixin の本体は次のようになります。
@mixin linear-gradient($angle, $color1, $offset1, $color2 ,$offset2:100, $color3:null, $offset3:100, $color4:null, $offset4:100, $color5:null, $offset5:100){
$angle_old:'';
$angle_webkit:'';
$angle_svg:'';
$color-stop1_css:'#'#{$color1}' '#{$offset1}'%';
$color-stop2_css:',#'#{$color2}' '#{$offset2}'%';
$color-stop3_css:'';
$color-stop4_css:'';
$color-stop5_css:'';
$color-stop1_svg:'%3cstop%20style%3d%22stop%2dcolor%3a%23'#{$color1}'%22%20offset%3d%22'#{$offset1}'%25%22%2f%3e';
$color-stop2_svg:'%3cstop%20style%3d%22stop%2dcolor%3a%23'#{$color2}'%22%20offset%3d%22'#{$offset2}'%25%22%2f%3e';
$color-stop3_svg:'';
$color-stop4_svg:'';
$color-stop5_svg:'';
$color-stop1_webkit:'color-stop('#{$offset1}'%, #'#{$color1}')';
$color-stop2_webkit:',color-stop('#{$offset2}'%, #'#{$color2}')';
$color-stop3_webkit:'';
$color-stop4_webkit:'';
$color-stop5_webkit:'';
@if $angle == "to bottom"{
$angle_old:'top';
$angle_webkit:'left top, left bottom';
$angle_svg:'%20x2%3d%220%25%22%20y2%3d%22100%25%22';
}
@if $angle == "to left"{
$angle_old:'right,';
$angle_webkit:'right top, left top';
$angle_svg:'%20x2%3d%22100%25%22';
}
@if $angle == "to top"{
$angle_old:'bottom,';
$angle_webkit:'left bottom, left top';
$angle_svg:'%20x2%3d%220%22%20y1%3d%22100%25%22';
}
@if $angle == "to right"{
$angle_old:'left,';
$angle_webkit:'left top, right top';
$angle_svg:'';
}
@if $color3 != null {
$color-stop3_css:',#'#{$color3}' '#{$offset3}'%';
$color-stop3_svg:'%3cstop%20style%3d%22stop%2dcolor%3a%23'#{$color3}'%22%20offset%3d%22'#{$offset3}'%25%22%2f%3e';
$color-stop3_webkit:',color-stop('#{$offset3}'%, #'#{$color3}')';
}
@if $color4 != null {
$color-stop4_css:',#'#{$color4}' '#{$offset4}'%';
$color-stop4_svg:'%3cstop%20style%3d%22stop%2dcolor%3a%23'#{$color4}'%22%20offset%3d%22'#{$offset4}'%25%22%2f%3e';
$color-stop4_webkit:',color-stop('#{$offset4}'%, #'#{$color4}')';
}
@if $color5 != null {
$color-stop5_css:',#'#{$color5}' '#{$offset5}'%';
$color-stop5_svg:'%3cstop%20style%3d%22stop%2dcolor%3a%23'#{$color5}'%22%20offset%3d%22'#{$offset5}'%25%22%2f%3e';
$color-stop5_webkit:',color-stop('#{$offset5}'%, #'#{$color5}')';
}
-pie-background: linear-gradient(#{$angle_old},#{$color-stop1_css}#{$color-stop2_css}#{$color-stop3_css}#{$color-stop4_css}#{$color-stop5_css});
background-image: url(data:image/svg+xml,%3c%3fxml%20version%3d%221%2e0%22%3f%3e%3csvg%20xmlns%3d%22http%3a%2f%2fwww%2ew3%2eorg%2f2000%2fsvg%22%20width%3d%22100%25%22%20height%3d%22100%25%22%3e%3cdefs%3e%3clinearGradient%20id%3d%22G%22#{$angle_svg}%3e#{$color-stop1_svg}#{$color-stop2_svg}#{$color-stop3_svg}#{$color-stop4_svg}#{$color-stop5_svg}%3c%2flinearGradient%3e%3c%2fdefs%3e%3crect%20width%3d%22100%25%22%20height%3d%22100%25%22%20fill%3d%22url%28%23G%29%22%2f%3e%3c%2fsvg%3e);
background-image: -webkit-gradient(linear,#{$angle_webkit},#{$color-stop1_webkit}#{$color-stop2_webkit}#{$color-stop3_webkit}#{$color-stop4_webkit}#{$color-stop5_webkit});
background-image:-webkit-linear-gradient(#{$angle_old},#{$color-stop1_css}#{$color-stop2_css}#{$color-stop3_css}#{$color-stop4_css}#{$color-stop5_css});
background-image: -moz-linear-gradient(#{$angle_old},#{$color-stop1_css}#{$color-stop2_css}#{$color-stop3_css}#{$color-stop4_css}#{$color-stop5_css});
background-image: -ms-linear-gradient(#{$angle_old},#{$color-stop1_css}#{$color-stop2_css}#{$color-stop3_css}#{$color-stop4_css}#{$color-stop5_css});
background-image: -o-linear-gradient(#{$angle_old},#{$color-stop1_css}#{$color-stop2_css}#{$color-stop3_css}#{$color-stop4_css}#{$color-stop5_css});
background-image: linear-gradient(#{$angle},#{$color-stop1_css}#{$color-stop2_css}#{$color-stop3_css}#{$color-stop4_css}#{$color-stop5_css});
}
さて、URL エンコード内では、colorStop 用の % や色コードの # をそのまま文字列として使うことができません。なので、冒頭で紹介した mixin の引数には % や # がついた状態ではなく、それらを取り除いた状態にしているわけです。SASS で文字列の replace ができればいいのですが…。ただ、これは内部で JavaScript のメソッドを利用可能な LESS でなら解決できる問題ですね。LESS いいですね。
というわけで、まとめとして
- CSS の gradient 関数は、SVG の Gradient にとても似ている
- SVG の内容はバイナリーではなくテキストなので動的な操作が簡単
- SASS より LESS が便利
ということがわかりました。
SASS では replace や文字列の加工などができませんが、LESS ならこれが可能です。おそらく、ここで示した mixin を LESS ように置き換えるならもっとシンプルになりそうです。
THREE.js で WebGL
JavaScript Advent Calendar 2011 (WebGL コース) の2日目として、この記事では THREE.js 経由で、WebGL を利用し 3D オブジェクトを表示するためのチュートリアルを紹介します。(WebGL とは、canvas 要素内で 3D を扱うための仕組みです)
この記事では最終的に次のデモを完成させます。
この記事で最終的に完成する demo (Chrome, Firefox, Opera などのWebGL が有効なブラウザー専用)
WebGL は普通に書くと OpenGL の shading の言語を理解していなければならず、よくわからないし難しいです。しかし、THREE.js のような WebGL のラッパーライブラリーを使うことで、JavaScript の知識の範囲で WebGL を扱うことができます。
THREE.js で 3D のグラフィクスを表示するためには…
- 下準備
- カメラを用意
- シーンを用意
- メッシュを作成
- ジオメトリーを作成
- マテリアルを作成
- メッシュをシーンに追加
- 光源を作成
- 光源をシーンに追加
- レンダラーを用意
- レンダラーをDOMに追加
- レンダリング
- アニメーション
といった手順を踏むことになります。(一部前後しても可能)
下準備
THREE.js を入手し、HTML 内に読み込みましょう。あわせて、領域の大きさも決めておきます。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>demo</title>
<script src="Three.js"></script>
<script>
window.addEventListener("DOMContentLoaded", function(){
var width = 600;
var height = 600;
});
</script>
</head>
<body>
</body>
</html>
カメラを用意
THREE.js のカメラにはいくつか種類があり、透視投影と平行投影のどちらの投影法も利用することができます。
| 透視投影 | THREE.PerspectiveCamera |
|---|---|
| 平行投影 | THREE.OrthographicCamera |

ここでは、透視投影を利用してみましょう。THREE.PerspectiveCamera には 画角, 縦横比, クリッピング手前, クリッピング奥 を渡します。クリッピング手前は、無限に近い場合を描画しないようにするため、クリッピング奥は、無限に遠い場所を描画しないようにするために設定します。つまり、クリッピングの手前と奥までの間が描画されることになります。
画角を変数 fov, 縦横比を変数 aspect, クリッピング手前を変数 near,クリッピング奥を変数 far としてカメラを設置するならば次のようになります。また、カメラの位置は x, y, z 方向に動かすこともできます。次の例では z 方向に 500 ずらしています。
<script>
window.addEventListener("DOMContentLoaded", function(){
var width = 600;
var height = 600;
var fov = 80;
var aspect = width / height;
var near = 1;
var far = 1000;
var camera = new THREE.PerspectiveCamera( fov, aspect, near, far );
camera.position.z = 500;
});
</script>
シーンを用意
シーンを用意します。シーンとは空間を意味し、この中にさまざまなオブジェクト (物体) などを詰め込み、配置していくことになります。
<script>
window.addEventListener("DOMContentLoaded", function(){
var width = 600;
var height = 600;
var fov = 80;
var aspect = width / height;
var near = 1;
var far = 1000;
var camera = new THREE.PerspectiveCamera( fov, aspect, near, far );
camera.position.z = 500;
var scene = new THREE.Scene();
});
</script>
メッシュを作成
メッシュとはポリゴンの集合により作られたオブジェクト (物体) のことを言います。メッシュは、ジオメトリー (座標) とマテリアル (表面素材) によって構成されます。
ジオメトリーを作成
ジオメトリーは、大きく分けて、
- あらかじめ THREE.js で用意されたプリミティブなジオメトリー
- 独自にモデリングしたジオメトリー
のどちらかを利用することができます。ここでは基本として、プリミティブなジオメトリーである、THREE.CubeGeometry を利用します。
THREE.CubeGeometry の引数には、横幅, 高さ, 奥行き, 横の分割数, 縦の分割数, 奥行きの分割数, 表面, sides を渡すことができます。横幅、高さ、奥行き以外はオプションです。sides は…何かよくわからなかったです。
ここでは、横幅、高さ、奥行きがそれぞれ 200 の立方体となるジオメトリーを用意しています。
<script>
window.addEventListener("DOMContentLoaded", function(){
var width = 600;
var height = 600;
var fov = 80;
var aspect = width / height;
var near = 1;
var far = 1000;
var camera = new THREE.PerspectiveCamera( fov, aspect, near, far );
camera.position.z = 500;
var scene = new THREE.Scene();
var geometry = new THREE.CubeGeometry(200, 200, 200);
});
</script>
ジオメトリーは、この他にも THREE.SphereGeometry : 球体、THREE.TextGeometry : テキスト
THREE.TorusGeometry : トーラス(ドーナツ状の形)
などさまざまあります。また、独自にモデリングしたジオメトリーを利用することもでき、その例は、この記事の最後に参考として添付しています。
マテリアルを作成
先ほど作成したジオメトリーの表面に割り当てるマテリアルを用意しておきます。THREE.js ではさまざまなマテリアル表現を選択できます。ここではランバート反射表現が可能な THREE.MeshLambertMaterial を利用します。
THREE.MeshLambertMaterial の引数には、さまざまなプロパティーをもった object を渡すことができ、例えば、color, opacity,
, blending などがあります。今回はごく単純に color のみを指定します。
<script>
window.addEventListener("DOMContentLoaded", function(){
var width = 600;
var height = 600;
var fov = 80;
var aspect = width / height;
var near = 1;
var far = 1000;
var camera = new THREE.PerspectiveCamera( fov, aspect, near, far );
camera.position.z = 500;
var scene = new THREE.Scene();
var geometry = new THREE.CubeGeometry(200, 200, 200);
var material = new THREE.MeshLambertMaterial( { color: 0x660000 } );
});
</script>
マテリアルはこの他にも THREE.MeshBasicMaterial や鏡面反射が可能 THREE.MeshPhongMaterial などさまざま用意されています。それぞれの引数に渡せる object のプロパティーは異なり、例えば THREE.MeshPhongMaterial には specular や shininess など輝きに関連する内容を渡すことができます。
メッシュを作成
ジオメトリーとマテリアルを合わせて一つのメッシュとします。THREE.Mesh の引数に ジオメトリー, マテリアル を渡します。
<script>
window.addEventListener("DOMContentLoaded", function(){
var width = 600;
var height = 600;
var fov = 80;
var aspect = width / height;
var near = 1;
var far = 1000;
var camera = new THREE.PerspectiveCamera( fov, aspect, near, far );
camera.position.z = 500;
var scene = new THREE.Scene();
var geometry = new THREE.CubeGeometry(200, 200, 200);
var material = new THREE.MeshLambertMaterial( { color: 0x660000 } );
var cubeMesh = new THREE.Mesh( geometry, material);
});
</script>
メッシュをシーンに追加
先ほど用意したメッシュをシーンに追加します。シーンに何かを追加する際には、シーン.add(メッシュ) とします。シーンは scene、メッシュは cubeMesh という名前で用意しているので、ここでは scene.add( cubeMesh ); となればいいわけです。
<script>
window.addEventListener("DOMContentLoaded", function(){
var width = 600;
var height = 600;
var fov = 80;
var aspect = width / height;
var near = 1;
var far = 1000;
var camera = new THREE.PerspectiveCamera( fov, aspect, near, far );
camera.position.z = 500;
var scene = new THREE.Scene();
var geometry = new THREE.CubeGeometry(200, 200, 200);
var material = new THREE.MeshLambertMaterial( { color: 0x660000 } );
var cubeMesh = new THREE.Mesh( geometry, material);
scene.add( cubeMesh );
});
</script>
光源を作成
次に、光源を作成します。光に影響する表面素材を利用している場合、光源がないと真っ暗で何も見えなくなってしまいます。
THREE.js では、環境光、平行光源、点光源、スポットライトを利用できます。
| 環境光 | THREE.AmbientLight |
|---|---|
| 平行光源 (無限遠光源) | THREE.DirectionalLight |
| 点光源 | THREE.PointLight |
| スポットライト | THREE.SpotLight |
この例では、太陽のように一定の方向で全体を照らしてくれる平行光源を利用します。もちろん、追加で複数の種類の光源を複数回利用することもできます。
THREE.DirectionalLight の引数には、色, 強さ を渡します。
また、真上からの光ではつまらないので少し斜めにしておきます。次のコードでは、平行光源.position.z で少し手前にずらしています。
<script>
window.addEventListener("DOMContentLoaded", function(){
var width = 600;
var height = 600;
var fov = 80;
var aspect = width / height;
var near = 1;
var far = 1000;
var camera = new THREE.PerspectiveCamera( fov, aspect, near, far );
camera.position.z = 500;
var scene = new THREE.Scene();
var geometry = new THREE.CubeGeometry(200, 200, 200);
var material = new THREE.MeshLambertMaterial( { color: 0x660000 } );
var cubeMesh = new THREE.Mesh( geometry, material);
scene.add( cubeMesh );
var directionalLight = new THREE.DirectionalLight( 0xffffff, 3 );
directionalLight.position.z = 3;
});
</script>
光源をシーンに追加
先ほど用意した光源をシーンに追加します。シーンにメッシュを追加する際と同様に、シーン.add(光源) とします。シーンは scene、先ほど作成した平行光源は directionalLight という名前で用意しているので、ここでは scene.add( directionalLight ); となればいいわけです。
<script>
window.addEventListener("DOMContentLoaded", function(){
var width = 600;
var height = 600;
var fov = 80;
var aspect = width / height;
var near = 1;
var far = 1000;
var camera = new THREE.PerspectiveCamera( fov, aspect, near, far );
camera.position.z = 500;
var scene = new THREE.Scene();
var geometry = new THREE.CubeGeometry(200, 200, 200);
var material = new THREE.MeshLambertMaterial( { color: 0x660000 } );
var cubeMesh = new THREE.Mesh( geometry, material);
scene.add( cubeMesh );
var directionalLight = new THREE.DirectionalLight( 0xffffff, 3 );
directionalLight.position.z = 3;
scene.add( directionalLight );
});
</script>
レンダラーを用意
ここまでの流れで、表示するための内容、つまり、カメラ、シーン、シーンの内容が揃いました。これらを実際に表示するための準備として、レンダラーを用意します。
THREE.js ではレンダリング方法ががあり、WebGL 意外にも、2Dcanvas, SVG を利用したレンダリングも可能です。ここではせっかくなので WebGL をつかったレンダリングを行います。
THREE.WebGLRenderer でレンダラーを用意し、レンダラー.setSize(横幅, 高さ) で大きさを決めておきます。
そして、renderer.domElement を DOM に appendChild し、Web ページ上にビューポートを設置します。このとき、WebGL でレンダリングさせている場合には、canvas要素が配置されます。
<script>
window.addEventListener("DOMContentLoaded", function(){
var width = 600;
var height = 600;
var fov = 80;
var aspect = width / height;
var near = 1;
var far = 1000;
var camera = new THREE.PerspectiveCamera( fov, aspect, near, far );
camera.position.z = 500;
var scene = new THREE.Scene();
var geometry = new THREE.CubeGeometry(200, 200, 200);
var material = new THREE.MeshLambertMaterial( { color: 0x660000 } );
var cubeMesh = new THREE.Mesh( geometry, material);
scene.add( cubeMesh );
var directionalLight = new THREE.DirectionalLight( 0xffffff, 3 );
directionalLight.position.z = 3;
scene.add( directionalLight );
var renderer = new THREE.WebGLRenderer();
renderer.setSize( width, height );
document.body.appendChild( renderer.domElement );
});
</script>
レンダリング
ここまでですべての準備が整いました。最後にレンダラー.render(シーン, カメラ)でレンダリングすることで、メッシュが光源の影響を受けて描画されます。
<script>
window.addEventListener("DOMContentLoaded", function(){
var width = 600;
var height = 600;
var fov = 80;
var aspect = width / height;
var near = 1;
var far = 1000;
var camera = new THREE.PerspectiveCamera( fov, aspect, near, far );
camera.position.z = 500;
var scene = new THREE.Scene();
var geometry = new THREE.CubeGeometry(200, 200, 200);
var material = new THREE.MeshLambertMaterial( { color: 0x660000 } );
var cubeMesh = new THREE.Mesh( geometry, material);
scene.add( cubeMesh );
var directionalLight = new THREE.DirectionalLight( 0xffffff, 3 );
directionalLight.position.z = 3;
scene.add( directionalLight );
var renderer = new THREE.WebGLRenderer();
renderer.setSize( width, height );
document.body.appendChild( renderer.domElement );
renderer.render( scene, camera );
});
</script>
アニメーション
ただ静止状態のまま表示しても、3D としてあまり面白くありません。アニメーションさせてみましょう。カメラやメッシュ、光源などを動かしながら連続でレンダリングすることでアニメーションさせることができます。
メッシュを少しずつ回転させながら連続でレンダリングする例は次のようになります。
<script>
window.addEventListener("DOMContentLoaded", function(){
var width = 600;
var height = 600;
var fov = 80;
var aspect = width / height;
var near = 1;
var far = 1000;
var camera = new THREE.PerspectiveCamera( fov, aspect, near, far );
camera.position.z = 500;
var scene = new THREE.Scene();
var geometry = new THREE.CubeGeometry(200, 200, 200);
var material = new THREE.MeshLambertMaterial( { color: 0x660000 } );
var cubeMesh = new THREE.Mesh( geometry, material);
scene.add( cubeMesh );
var directionalLight = new THREE.DirectionalLight( 0xffffff, 3 );
directionalLight.position.z = 3;
scene.add( directionalLight );
var renderer = new THREE.WebGLRenderer();
renderer.setSize( width, height );
document.body.appendChild( renderer.domElement );
renderer.render( scene, camera );
function rendering(){
cubeMesh.rotation.x += 0.01;
cubeMesh.rotation.y += 0.01;
renderer.render( scene, camera );
setTimeout(rendering, 30);
}
rendering();
});
</script>
これで冒頭のデモが完成します。
まとめ
ここまで見てきたとおり、THREE.js は、JavaScript と、3DCG に対する少しの知識があれば、WegGL を扱うことができるライブラリーというわけです。
WebGL は敷居が高いと感じているフロントエンドの技術者さんも多いのではないでしょうか。しかし、THREE.js はフロントエンドの技術者でも WebGL を触りやすくしてくれているのです。どんどん試してみるといいんじゃぁないでしょうか!
あ、あと THREE.js をつかって、モデラーで作成した 3D オブジェクトも扱うことができます。

デモ。 (Chrome, Firefox, Opera などのWebGL が有効なブラウザー専用)