SOURCE

console 命令行工具 X clear

                    
>
console
/*
  Title: Yet Another Physics Experiment
  Author: Alexey Astafiev <alexey.astafiev@gmail.com>
  Www: https://github.com/lamaster
  Description:
    Physics Experiment, based on verlet integration and shapeMatching
    This is just a proof of concept, so code is not good and fast enought
    
    features to add: 
      friction between objects
      better collision detection
*/

var Point = function(x, y) {
  this.current = {
    x: x,
    y: y
  };
  this.previous = {
    x: x,
    y: y
  };
  
  // intuitive property access
  this.__defineGetter__('x', function() {
    return this.current.x;
  });
  this.__defineGetter__('y', function() {
    return this.current.y;
  });
};

Object.assign(Point.prototype, {
  integrate: function() {
    if (this.isStatic){ return };

    var current = this.current,
        previous = this.previous,
        x = current.x,
        y = current.y;

    current.x += x - previous.x;
    current.y += y - previous.y + .28;

    previous.x = x;
    previous.y = y;
  },
  getDisplacement: function(centerOfMass) {
    var x = this.current.x - centerOfMass.x,
        y = this.current.y - centerOfMass.y;

    return {
      x: x,
      y: y
    };
  }
});

var Body = function(points) {
  this.points = points;
  this.original = []; // position relative to centerOfMass;
  this.centerOfMass = this.getCenterOfMass();
  this.angle = 0;
  this.stiffness = 1;
  this.configure();
  this.min = {};
  this.max = {};
  this.type = Body.POLY;
  this.zindex = Body.zindex++;
};

Body.POLY = 0;
Body.LINE = 1;
Body.zindex = 0;

Body.prototype = Object.assign(Body.prototype, {
  configure: function() {
    var points = this.points || [],
        l = points.length,
        centerOfMass = this.getCenterOfMass();

    while (l--) {
      var point = points[l];
      this.original[l] = this.original[l] || {};
      this.original[l].x = point.current.x - centerOfMass.x;
      this.original[l].y = point.current.y - centerOfMass.y;
    }
  },
  getCenterOfMass: function(original) {
    var points = this.points,
        current = original ? "original" : "current",
        l = points.length,
        x = 0,
        y = 0;

    while (l--) {
      x += points[l][current].x;
      y += points[l][current].y;
    }

    return {
      x: x / points.length,
      y: y / points.length
    };
  },
  shapeMatch: function() {
    var centerOfMass = this.getCenterOfMass(),
        angleDelta = 0;

    var points = this.points,
        l = points.length;

    // finding summ of all angles betwen current and previous displacement vectors
    while (l--) {
      var point = points[l],
          displacement = point.getDisplacement(centerOfMass);

      var cos = displacement.x * this.original[l].x + displacement.y * this.original[l].y, // cos = a * b
          sin = displacement.y * this.original[l].x - displacement.x * this.original[l].y; // sin = a ^ b

      angleDelta += Math.atan2(sin, cos);
    }

    angleDelta /= points.length;

    this.angleDelta = angleDelta;
    this.angle += angleDelta;
    this.angle %= Math.PI * 2;
  },
  updatePositions: function() {
    // moving points to the goal position
    var points = this.points,
        centerOfMass = this.getCenterOfMass(),

        cos = Math.cos(this.angleDelta),
        sin = Math.sin(this.angleDelta);

    var l = points.length;
    while (l--) {
      var point = points[l],
          goalX = cos * this.original[l].x - sin * this.original[l].y,
          goalY = sin * this.original[l].x + cos * this.original[l].y;

      // updating rest position to prevent flipping
      this.original[l].x = goalX;
      this.original[l].y = goalY;

      goalX += centerOfMass.x;
      goalY += centerOfMass.y;

      point.current.x += (goalX - point.current.x) * this.stiffness;
      point.current.y += (goalY - point.current.y) * this.stiffness;
    }

    this.centerOfMass.x = centerOfMass.x;
    this.centerOfMass.y = centerOfMass.y;
  },
  updateAABB: function() {
    var minX = this.points[0].current.x,
        minY = this.points[0].current.y,
        maxX = this.points[0].current.x,
        maxY = this.points[0].current.y;

    for (var i = 1, l = this.points.length; i < l; i++) {
      var point = this.points[i];

      minX = Math.min(point.current.x, minX);
      minY = Math.min(point.current.y, minY);

      maxX = Math.max(point.current.x, maxX);
      maxY = Math.max(point.current.y, maxY);
    }

    this.max.x = maxX;
    this.max.y = maxY;
    this.min.x = minX;
    this.min.y = minY;
  }
})

var World = function(gravity, width, height) {
  this.points = [];
  this.bodies = [];
  this.constraints = [];

  this.width = width;
  this.height = height;
  this.gravity = gravity;
  this.isStarted = true;
}

Object.assign(World.prototype, {
  addPoint: function(point) {
    if (Array.isArray(point)) {
      for (var i = 0, l = point.length; i < l; i++){
        this.addPoint(point[i]);
      }
    } else {
      !~this.points.indexOf(point) && this.points.push(point);
    }
    return this;
  },
  addBody: function(body) {
    return body && !~this.bodies.indexOf(body) && this.bodies.push(body) && this.addPoint(body.points);
  },
  shapeMatch: function() {
    var l = this.bodies.length;
    while (l--) {
      this.bodies[l].shapeMatch();
      this.bodies[l].updateAABB();
      this.bodies[l].updatePositions();
    }
    return this;
  },
  updatePositions: function() {
    var l = this.bodies.length;
    while (l--) {
      this.bodies[l].updatePositions();
    }
    return this;
  },
  bounds: function() {
    var points = this.points,
        i = points.length;

    while (i--) {
      var point = points[i],
          x = point.current.x,
          y = point.current.y;

      point.current.x = Math.max(0, Math.min(this.width, x));
      point.current.y = Math.max(0, Math.min(this.height, y));

      if (y >= this.height) {
        point.current.x -= (x - point.previous.x) * .1;
      }
    }
    return this;
  },
  integrate: function() {
    var points = this.points,
        i = points.length;

    while (i--) {
      points[i].integrate();
    }

    return this;
  },
  getProjection: function(axis, points) {
    var dot = points[0].x * axis.x + points[0].y * axis.y,
        min = max = dot;

    for (var i = 1; i < points.length; i++) {
      var point = points[i];
  
      dot = point.x * axis.x + point.y * axis.y;

      if (dot < min) {
        min = dot;
      } else if (dot > max){
        max = dot
      };
    }

    return {
      min: min,
      max: max
    };
  },
  distance: function(A, B) {
    return A.min < B.min ? B.min - A.max : A.min - B.max;
  },
  overlap: function(A, B) {
    return A.max.x > B.min.x && A.min.x < B.max.x && A.max.y > B.min.y && A.min.y < B.max.y;
  },
  detectCollisions: function() {
    this.bodies.sort(function(a, b) {
      return a.min.x > b.min.x ? 1 : -1;
    });

    for (var i = 0, l = this.bodies.length; i < l; i++) {
      var A = this.bodies[i];

      for (var j = i + 1; j < l && A.max.x > this.bodies[j].min.x; j++) {
        var B = this.bodies[j];

        if (A.type !== Body.POLY || B.type !== Body.POLY) {
          continue;
        }

        if (this.overlap(A, B)) {
          var data = this.getCollisionData(A, B);

          if (data) {
            this.resolveCollisions(A, B, data);
          }
        }
      }
    }
    return this;
  },
  getCollisionData: function(A, B) {
    var aLength = A.points.length,
        bLength = B.points.length;

    var depth = Number.POSITIVE_INFINITY,
        edge = [],
        bBody = B,
        aBody = A,
        axis, normal, contact;

    for (var i = 0, l = aLength + bLength; i < l; i++) {
      var body = i < aLength ? A : B,
          j = i < aLength ? i : i - aLength,
          n = (j + 1) === body.points.length ? 0 : j + 1;

      var a = body.points[j],
          b = body.points[n];

      axis = {
        x: a.y - b.y,
        y: b.x - a.x
      };

      var invLength = 1 / Math.sqrt(axis.x * axis.x + axis.y * axis.y);

      axis.x *= invLength;
      axis.y *= invLength;

      var projectionA = this.getProjection(axis, A.points),
          projectionB = this.getProjection(axis, B.points),
          distance = this.distance(projectionA, projectionB);

      if (distance > 0) {
        return false
      } else if (Math.abs(distance) < depth) {
        depth = Math.abs(distance);
        normal = axis;
        edge[0] = body.points[j];
        edge[1] = body.points[n];
        bBody = body;
      }
    }

    if (bBody !== B) {
      bBody = A;
      aBody = B;
    }

    var centerX = aBody.centerOfMass.x - bBody.centerOfMass.x,
        centerY = aBody.centerOfMass.y - bBody.centerOfMass.y;

    if ((normal.x * centerX + normal.y * centerY) < 0) {
      normal.x = -normal.x;
      normal.y = -normal.y
    };

    var collisionVector = {
      x: normal.x * depth,
      y: normal.y * depth
    };

    var smallest = Number.POSITIVE_INFINITY;
    for (var i = 0, l = aBody.points.length; i < l; i++) {
      var point = aBody.points[i],

          x = point.current.x - aBody.centerOfMass.x,
          y = point.current.y - aBody.centerOfMass.y,

          distance = normal.x * x + normal.y * y;

      if (distance < smallest) {
        smallest = distance;
        contact = point;
      }
    }


    return {
      depth: depth,
      edge: edge,
      normal: normal,
      vector: collisionVector,
      contact: contact
    };
  },
  resolveCollisions: function(A, B, collision) {
    var a = collision.edge[0],
        b = collision.edge[1];

    var T;

    if (Math.abs(a.x - b.x) > Math.abs(a.y - b.y)){
      T = (collision.contact.x - collision.vector.x - a.x) / (b.x - a.x);
    } else {
      T = (collision.contact.x - collision.vector.y - a.y) / (b.y - a.y);
    }

    var lambda = 1.0 / (T * T + (1 - T) * (1 - T));

    a.current.x -= collision.vector.x * (1 - T) * 0.5 * lambda;
    a.current.y -= collision.vector.y * (1 - T) * 0.5 * lambda;

    b.current.x -= collision.vector.x * T * 0.5 * lambda;
    b.current.y -= collision.vector.y * T * 0.5 * lambda;

    collision.contact.current.x += collision.vector.x * 0.5;
    collision.contact.current.y += collision.vector.y * 0.5;

    // @DEBUG_RENDER
    // renderer.drawLine(a,b,"red",4);
    // renderer.drawCircle(collision.contact, "red", 10);
  },
  pause: function() {
    this.started = false;
  },
  resume: function() {
    this.started = true;
    update();
  },
  findClosestPoint: function(input, radius) {
    var closest,
        points = this.points,
        radius = radius || 5;

    for (var i = 0, l = points.length; i < l; i++) {
      var current = points[i].current;
      if (
        (current.x >= input.x - radius) && 
        (current.x <= input.x + radius) && 
        (current.y >= input.y - radius) && 
        (current.y <= input.y + radius)
      ){
        closest = points[i];
      };
    }
    return closest;

  }
})


// HELPERS 

var CanvasRenderer = function(canvas) {
  this.canvas = canvas;
  this.width = canvas.width;
  this.height = canvas.height;

  var g = this.g = canvas.getContext('2d');

  
  /*  Define methods in a constructor 
    instead of prototype to keep it simple. 
    Not a good practice, but makes canvas
    context easier to access. */

  this.render = function(world) {
    var bodies = world.bodies.slice().sort(function(a, b) {
      return a.zindex < b.zindex ? 1 : -1
    })
    for (var i = 0, l = bodies.length; i < l; i++) {
      var body = bodies[i];
      switch (body.type) {
        case Body.LINE:
          this.drawLine(body.points[0], body.points[1]);
          break;
        default:
          this.drawPoly(body.points, body.color || 0x0);
      }
    }
  };

  this.clear = function() {
    g.clearRect(0, 0, this.width, this.height);
  };

  this.drawCircle = function(position, color, size) {
    g.save();
    g.fillStyle = color || 0x0;
    g.beginPath();
    g.arc(position.x, position.y, size || 5, 0, Math.PI * 2, false);
    g.fill();
    g.restore();
  };

  this.drawLine = function(a, b, color, width) {
    g.save();
    g.strokeStyle = color || 0x0;
    g.lineWidth = width || 1;
    g.beginPath();
    g.moveTo(a.x, a.y);
    g.lineTo(b.x, b.y);
    g.stroke();
    g.restore();
  };

  this.drawPoly = function(points, color) {
    var l = points.length,
        firstPoint = points[l - 1].current;

    g.save();
    g.fillStyle = color || 0x0;
    g.beginPath();
    g.moveTo(firstPoint.x, firstPoint.y);
    while (l--) {
      var point = points[l].current;
      g.lineTo(point.x, point.y);
    }
    g.fill();
    g.restore();
  };

  this.drawRect = function(min, max, rotation, color) {
    g.save();
    g.fillStyle = color || 0x0;
    g.beginPath();
    g.rect(min.x, min.y, max.x - min.x, max.y - min.y);
    g.fill();
    g.restore();
  }
};

var InputController = function(canvas, world) {
  this.x = 0;
  this.y = 0;
  this.canvas = canvas;
  this.world = world;
  this.isPressed = false;

  var self = this;
  
  this.up = function(event) {
    event.preventDefault();
    self.isPressed = false;
  }

  this.down = function(event) {
    event.preventDefault();
    self.isPressed = true;
    var event = event.changedTouches ? event.changedTouches[0] : event;

    self.x = event.offsetX;
    self.y = event.offsetY;
    self.closestPoint = self.world.findClosestPoint(self, 50);
  }

  this.move = function(event) {
    event.preventDefault();
    var event = event.changedTouches ? event.changedTouches[0] : event;

    self.touches = event.changedTouches;
    self.x = event.offsetX;
    self.y = event.offsetY;
    console.log(event)
  }

  // mouse events
  canvas.addEventListener('mouseup', this.up, false);
  canvas.addEventListener('mousedown', this.down, false);
  document.addEventListener('mousemove', this.move, false);

  // touch events
  //document.addEventListener('touchend', this.up, false);
  //document.addEventListener('touchstart', this.down, false);
  //document.addEventListener('touchmove', this.move, false);
};

// simple factory function
function createBox(x, y, width, height){
  var box = new Body([
    new Point(x, y), new Point(x, y + height),
    new Point(x + width, y + height), new Point(x + width, y)
  ]);
  return box;
}


/*  Implementation example */

var renderer = new CanvasRenderer(document.getElementById('canvas')),
    gravity = {
      x: 0,
      y: 9.8
    },
    world = new World(gravity, renderer.width, renderer.height),
    input = new InputController(renderer.canvas, world);


var x = world.width / 2,
    y = 100;


var box = createBox(0, 0, 50, 50);
var box2 = createBox(50, 50, 50, 50);

var triangle = new Body([
  new Point(x, y),
  new Point(x - 50, y + 100), new Point(x + 50, y + 100)
  ]);

// soft yellow box
box2.stiffness = .20 / 3;
box2.color = "yellow";

// rigid black box
box.color = "black";
triangle.color = "blue";

var yello2 = createBox(50, 50, 50, 50);
yello2.stiffness = .20 / 3;
yello2.color = "green";

world
  .addBody(box)
  .addBody(box2)
  .addBody(yello2)
  .addBody(triangle)


// random generated bodies
for(var i = 0; i < 20; i++){
  var randomBox = createBox(i * 2, i * 5, Math.random()*30,Math.random()*50);
  randomBox.stiffness = 1;
 
  world.addBody(randomBox)

}

// simple rope
var point1 = new Point(world.width / 2, 0);
var point2 = new Point(world.width / 2, 50);
var point3 = triangle.points[0]
var chunk1 = new Body([point1, point2]);
var chunk2 = new Body([point2, point3]);

point1.isStatic = true;

chunk1.type = Body.LINE;
chunk2.type = Body.LINE;

world.addBody(chunk1);
world.addBody(chunk2);


(function update() {
  renderer.clear();

  box.color = "black";
  box2.color = "yellow";
  triangle.color = "blue";

  
  point1.current.x = world.width / 2;
  point1.current.y = 0;

  if (input.isPressed && input.closestPoint) {
    input.closestPoint.current.x += (input.x - input.closestPoint.current.x) * .1;
    input.closestPoint.current.y += (input.y - input.closestPoint.current.y) * .1;

    renderer.drawLine(input, input.closestPoint.current);
    renderer.drawCircle(input.closestPoint.current, 'red');
  }

  world.shapeMatch()

  var iterations = 5;
  while (iterations--) {
    world.detectCollisions().shapeMatch().bounds()
  }

  world.integrate()
  world.isStarted && requestAnimationFrame(update);

  renderer.render(world);
})()
<canvas id="canvas" width="630" height="490"></canvas>
* { margin: 0; padding:0 }
body {
  background: #333;
}
canvas { display: block; border:none; background-color: #888; margin: 0 auto; }