物理エンジン「Box2D.js」を使って、ボールが落下する自然な動きを表現することができました。
実現するのは結構難しいですが目を引くページになります。
Box2D.jsのダウンロードはここ
HTML
<div id="contents" class="bcolor"> <ul id="tag-ul" class="tag-cloud"> </ul> </div> <form id="btn"> <input id="exec" type="button" value="落下"> </form>
css
.rigidbody { position: absolute; left: 0; top: 0; background-color: gray; } #contents { position: relative; width:700px; height:550px; margin:0 auto; overflow: hidden; } #contents.bcolor { background-color:#f5f5dc; } .tag-cloud { list-style: none; } .tag-cloud__tag-item { display: none; width: 90px; height: 90px; line-height: 90px; text-align: center; background-color: #ffb100; visibility:hidden; margin-left:0 !important; } .tag-cloud__tag-item a { display: block; width: 100%; height: 100%; color: #fff; border-radius: 50%; font-weight: bold; } .tag-cloud__tag-item a:hover { background-color: rgba(255, 255, 255, 0.3); } .tag-cloud__tag-item.item_id-1, .tag-cloud__tag-item.item_id-7, .tag-cloud__tag-item.item_id-17, .tag-cloud__tag-item.item_id-8, .tag-cloud__tag-item.item_id-3, .tag-cloud__tag-item.item_id-6 { background-color: #e21e17; } .tag-cloud__tag-item.item_id-4, .tag-cloud__tag-item.item_id-5, .tag-cloud__tag-item.item_id-9, .tag-cloud__tag-item.item_id-14, .tag-cloud__tag-item.item_id-15, .tag-cloud__tag-item.item_id-12 { background-color: #1081f9; } .tag-cloud__tag-item.item_id-10, .tag-cloud__tag-item.item_id-2, .tag-cloud__tag-item.item_id-11, .tag-cloud__tag-item.item_id-13, .tag-cloud__tag-item.item_id-16, .tag-cloud__tag-item.item_id-18, .tag-cloud__tag-item.item_id-19 { background-color: #651e94; } .tag-cloud__tag-item.item_id-4 { width: 170px; height: 170px; line-height: 170px; } .tag-cloud__tag-item.item_id-18 { width: 160px; height: 160px; line-height: 160px; } .tag-cloud__tag-item.item_id-13, .tag-cloud__tag-item.item_id-14 { width: 100px; height: 100px; line-height: 100px; } .tag-cloud__tag-item.item_id-8 { width: 140px; height: 140px; line-height: 140px; } .tag-cloud__tag-item.item_id-15 { width: 120px; height: 120px; line-height: 120px; } .tag-cloud__tag-item.item_id-3 { width: 130px; height: 130px; line-height: 130px; } form#btn { margin-top:10px; }
JavaScript
/* 1mに相当するpx */ var meterPerPixel = 30; var RAD_TO_DEG = 180 / Math.PI; var b2Vec2; var b2BodyDef; var b2Body; var b2FixtureDef; var b2World; var b2MassData; var b2PolygonShape; var b2CircleShape; //対応しているプロパティ名を取得 var transformProp = (function() { var style = document.createElement('div').style; var prefix = [ 'transform', 'webkitTransform', 'mozTransform', 'msTransform' ]; for (var i = 0, l = prefix.length; i < l; i++) { if (prefix[i] in style) { return prefix[i]; } } return 'transform'; }()); function defaultParam(data, defaults) { for (var key in defaults) { if (!data[key]) { data[key] = defaults[key]; } } } /** Rigidbody(剛体) * @param {b2World} world 物理エンジン世界 * @param {Object} data デフォルトパラメータ */ function RigidBody(world, data) { //デフォルトパラメータを設定 defaultParam(data, { density: 1.0, friction: 0.5, restitution: 0.2, shapeType: RigidBody.shapeType.BOX }); if (data.shapeType === RigidBody.shapeType.CIRCLE) { data.width = data.height = data.radius * 2; data.radius = (data.radius || 1) / meterPerPixel; } /* -------------------------------------------------------- 各種パラメータの保存 ----------------------------------------------------------- */ this.width = data.width; this.height = data.height; this.halfWidth = data.width * 0.5; this.halfHeight = data.height * 0.5; this.target = data.target; // 剛体の「性質」情報の生成 var fixDef = new b2FixtureDef(); //密度 fixDef.density = data.density; //摩擦係数 fixDef.friction = data.friction; //反発係数 fixDef.restitution = data.restitution; var halfWidthPerPixel = this.halfWidth / meterPerPixel; var halfHeightPerPixel = this.halfHeight / meterPerPixel; //剛体の形状 if (data.shapeType === RigidBody.shapeType.BOX) { fixDef.shape = new b2PolygonShape(); fixDef.shape.SetAsBox(halfWidthPerPixel, halfHeightPerPixel); } else if (data.shapeType === RigidBody.shapeType.CIRCLE) { fixDef.shape = new b2CircleShape(); fixDef.shape.SetRadius(data.radius); } // 剛体の「姿勢」情報の生成 var bodyDef = new b2BodyDef(); //剛体のタイプ bodyDef.type = data.type != null ? data.type : b2Body.b2_dynamicBody; //剛体の位置 var x = (data.x != null ? data.x : this.halfWidth) / meterPerPixel; var y = (data.y != null ? data.y : this.halfHeight) / meterPerPixel; bodyDef.position.x = x; bodyDef.position.y = y; //剛体の速度の減衰率の設定 bodyDef.linearDamping = data.linearDamping != null ? data.linearDamping : 0.0; bodyDef.angularDamping = data.angularDamping != null ? data.angularDamping : 0.01; //剛体適用要素が指定されいなければ生成する this.el = (data.el != null) ? data.el : document.createElement('div'); //剛体のパラメータをDOMのパラメータに設定 this.el.className += ' rigidbody'; if (data.shapeType === RigidBody.shapeType.BOX) { this.el.style.width = this.width + 'px'; this.el.style.height = this.height + 'px'; } else if (data.shapeType === RigidBody.shapeType.CIRCLE) { this.el.style.height = this.el.style.width = (data.radius * 2 * meterPerPixel) + 'px'; this.el.style.borderRadius = '50%'; } //targetが指定されている場合はそこに追加 this.target && this.target.appendChild(this.el); //設定情報を元に剛体を生成 this.body = world.CreateBody(bodyDef); //生成した剛体に「性質」を適用 this.body.CreateFixture(fixDef); } RigidBody.prototype = { constructor: RigidBody, /** * 剛体の情報をHTML要素に適用する */ applyToDOM: function () { var position = this.body.GetPosition(); //meterPerPixelに応じて位置を補正 var x = position.x * meterPerPixel - this.halfWidth; var y = position.y * meterPerPixel - this.halfHeight; var r = this.body.GetAngle() * RAD_TO_DEG; x = Math.abs(x) <= 0.0000001 ? 0 : x; y = Math.abs(y) <= 0.0000001 ? 0 : y; r = Math.abs(r) <= 0.0000001 ? 0 : r; this.el.style[transformProp] = 'translate(' + x + 'px, ' + y + 'px) rotate(' + r + 'deg)'; if(this.el.style.visibility != 'visible'){ this.el.style.visibility = 'visible'; } } } /** * Static member */ RigidBody.shapeType = { BOX: 'box', CIRCLE: 'circle' } // 初期化処理 function init(){ // ショートカット用にインポート b2Vec2 = Box2D.Common.Math.b2Vec2; b2BodyDef = Box2D.Dynamics.b2BodyDef; b2Body = Box2D.Dynamics.b2Body; b2FixtureDef = Box2D.Dynamics.b2FixtureDef; b2World = Box2D.Dynamics.b2World; b2MassData = Box2D.Collision.Shapes.b2MassData; b2PolygonShape = Box2D.Collision.Shapes.b2PolygonShape; b2CircleShape = Box2D.Collision.Shapes.b2CircleShape; var list = [ '<li class="tag-cloud__tag-item item_id-1"><a href="#">日本</a></li>', '<li class="tag-cloud__tag-item item_id-2"><a href="#">アメリカ</a></li>', '<li class="tag-cloud__tag-item item_id-3"><a href="#">中国</a></li>', '<li class="tag-cloud__tag-item item_id-4"><a href="#">イタリア</a></li>', '<li class="tag-cloud__tag-item item_id-5"><a href="#">イギリス</a></li>', '<li class="tag-cloud__tag-item item_id-6"><a href="#">インド</a></li>', '<li class="tag-cloud__tag-item item_id-7"><a href="#">韓国</a></li>', '<li class="tag-cloud__tag-item item_id-8"><a href="#">フランス</a></li>', '<li class="tag-cloud__tag-item item_id-9"><a href="#">マレーシア</a></li>', '<li class="tag-cloud__tag-item item_id-10"><a href="#">ドイツ</a></li>', '<li class="tag-cloud__tag-item item_id-11"><a href="#">ブラジル</a></li>', '<li class="tag-cloud__tag-item item_id-12"><a href="#">ノルウェー</a></li>', '<li class="tag-cloud__tag-item item_id-13"><a href="#">アルゼンチン</a></li>', '<li class="tag-cloud__tag-item item_id-14"><a href="#">カナダ</a></li>', '<li class="tag-cloud__tag-item item_id-15"><a href="#">メキシコ</a></li>', '<li class="tag-cloud__tag-item item_id-16"><a href="#">スエーデン</a></li>', '<li class="tag-cloud__tag-item item_id-17"><a href="#">ケニア</a></li>', '<li class="tag-cloud__tag-item item_id-18"><a href="#">サウジアラビア</a></li>', '<li class="tag-cloud__tag-item item_id-19"><a href="#">インドネシア</a></li>' ]; $('.tag-cloud').empty(); for(var i=0; i<list.length ; i++){ $(list[i]).appendTo('.tag-cloud'); } //剛体管理用配列 var rigidBodies = []; var target = document.getElementById('contents'); //静止状態でスリープに入るかどうか var allowSleep = true; //重力 var gravity = new b2Vec2(0, 9.8); //物理演算ワールドを生成 var world = new b2World(gravity, allowSleep); //地面を生成 var ground = new RigidBody(world, { target: target, type: b2Body.b2_staticBody, width: 1600, height: 20, x: 10, y: 500 }); rigidBodies.push(ground); //左の壁を生成 var leftWall = new RigidBody(world, { target: target, type: b2Body.b2_staticBody, width: 10, height: 550, x: -5, y: 225 }); rigidBodies.push(leftWall); //右の壁を生成 var rightWall = new RigidBody(world, { target: target, type: b2Body.b2_staticBody, width: 10, height: 550, x: 705, y: 225 }); rigidBodies.push(rightWall); var tagItems = document.querySelectorAll('.tag-cloud__tag-item'); var cnt = tagItems.length; (function loop() { cnt--; if(cnt < 0) { return; } setTimeout(loop, 240); tagItems[cnt].style.display = 'block'; var bit = new RigidBody(world, { el: tagItems[cnt], shapeType: RigidBody.shapeType.CIRCLE, radius: tagItems[cnt].offsetWidth / 2, x: Math.random() * 800, y: -80 - Math.random() * 200 }); bit.body.SetAngularVelocity(Math.random() * 10); rigidBodies.push(bit); }()); // update (function update() { requestAnimationFrame(update); //物理演算ワールドの時間を進める world.Step( 0.016, //60FPSは16ミリ秒程度 10, //velocityIterations 10 //positionIterations ); //計算された結果をHTMLに適用 for (var i = 0, len = rigidBodies.length; i < len; i++) { //rigidBodiesは初期化時に定義された、生成した剛体を格納しているただの配列。 rigidBodies[i].applyToDOM(); } // world.DrawDebugData(); world.ClearForces(); }()); } $(function(){ $('#exec').click(init); });