物理エンジン「Box2D」

物理エンジン「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);
});

サンプル