2Dphysics v.1.0


The World's smallest 2D physics engine in JavaScript including springs and joints


Commented source
(20kb)
Minified
(1.5kb zipped)
Micro
(no joints, 1.2kb zipped)


Source code


G=[0,.1],O=.4,E=.01,R=20,H=[],J=[],M=[],RECTANGLE=0,CIRCLE=1,SPRING=0,REPULSIVE=1,HINGE=2,FIXED=3;
((n=x=>S(x,1/l(x)),l=x=>d(x,x)**.5,d=(x,y)=>x[0]*y[0]+x[1]*y[1],p=(x,y)=>[x[0]+y[0],x[1]+y[1]],s=(x,y)=>p(x,S(y,-1)),S=(x,y)=>[x[0]*y,x[1]*y],C=(x,y)=>x[0]*y[1]-x[1]*y[0],r=(x,y,l)=>[(x[0]-y[0])*Math.cos(l)-(x[1]-y[1])*Math.sin(l)+y[0],(x[0]-y[0])*Math.sin(l)+(x[1]-y[1])*Math.cos(l)+y[1]],x=(x,y)=>[-x.A*y[1],x.A*y[0]],I=(c,t,l,e,a,n,r=1,o)=>{r&&(o=S(e,l),c.v=s(c.v,S(o,c.M)),t.v=p(t.v,S(o,t.M))),c.R&&(c.A-=a*l*c.I),t.R&&(t.A+=n*l*t.I)})=>{shape=(t,c,m=1,w=9,h=9,g,n=t?m*w*w/2:m*(w*w+h*h)/12,s={t,m,w,h,c:[0,0],f:.5,r:.5,a:0,v:[0,0],A:0,g:G,F:[0,0],T:0,R:1,p:[],N:[],n:[[0,-1],[1,0],[0,1],[-1,0]],M:m?1/m:0,I:n?1/n:0,V:[[-w/2,-h/2],[w/2,-h/2],[w/2,h/2],[-w/2,h/2]],e:H.length,...g})=>(transform(s,c,s.a),H.push(s),s),anchor=(t,c)=>(t.p.push(p(c,t.c)),t.p.length-1),joint=(t,A,a,B,b,s,l)=>{if(t>1){A.N.push(B.e);B.N.push(A.e);l=B.a-A.a}J.push({t,A,a,B,b,s,l})},transform=(c,f,n,i=4)=>{if(c.c=p(c.c,f),!c.t)for(;i--;)c.n[i]=r(c.n[i],[0,0],n),c.V[i]=r(p(c.V[i],f),c.c,n);for(i in c.p)c.p[i]=r(p(c.p[i],f),c.c,n)},run=(a,f,t,c,e,h,o,r,A,v,w,i,V,b,B,m,g)=>{for(M=[],f=H.length;f--;)for(a=f-1;a>=0;a--)if(c=H[f],e=H[a],c.F=S(c.g,c.m),c.M+e.M&&!c.N.includes(a)){if(h=0,c.t&&e.t)o=s(e.c,c.c),(r=l(o))=0&&w>0&&wA&&(A=w,v={v:e.V[i],d:w});v||(h=0),h&&v.d0&&(b=C(V,w=n(w)),B=C(i,w),m=Math.min(c.f,e.f),g=-d(h,w)/(c.M+e.M+b*b*c.I+B*B*e.I),Math.abs(g)>Math.abs(v)*m&&(g=Math.sign(g)*Math.abs(v)*m),I(c,e,g,w,b,B));for(t of J)c=t.A,e=t.B,h=c.p[t.a],o=e.p[t.b],v=s(h,c.c),w=s(o,e.c),A=s(o,h),i=(r=l(A))>0?n(A):[1,0],V=C(v,i),b=C(w,i),(B=c.M+e.M+V*V*c.I+b*b*e.I+E)&&(t.t?t.t<2?I(c,e,t.s*Math.max(0,t.l-r)/1e4,i,V,b):(I(c,e,(-d(s(p(e.v,x(e,w)),p(c.v,x(c,v))),i)+-O*r)/B,i,V,b),t.t>2&&(m=e.a-c.a-t.l,(g=c.I+e.I)&&I(c,e,(-(e.A-c.A)+-O*m)/g,0,1,1,0)),r>0&&(B=c.M+e.M,transform(c,S(A,c.M/B),0),transform(e,S(A,-e.M/B),0))):(I(c,e,-t.s*(r-t.l)/1e3/B,i,V,b),I(c,e,-t.s*(r-t.l)/1e3/B,i,V,b)))}for(f of H)f.M&&(f.v=p(f.v,S(f.F,f.M<0?-f.M:f.M)),f.F=[0,0],f.A+=f.T*f.I,f.a+=f.A,transform(f,f.v,f.A),f.v=S(f.v,.99),Math.abs(l(f.v))<1e-4&&(f.v=[0,0]),f.A*=.99,Math.abs(f.A)<1e-4&&(f.A=0),f.T=0)}})()

API

Sample code snippets are available under the demo ↓

GLOBALS
  • G = [0,.1] (Gravity)
  • O = .4 (Shapes overlap correction)
  • E = .01 (Fixed joints elasticity)
  • R = 20 (Resolutions per frame)
  • H = [] (shapes list)
  • J = [] (joints list)
  • M = [] (manifolds list)
SHAPES TYPES
  • RECTANGLE = 0;
  • CIRCLE = 1;
JOINTS TYPES
  • SPRING = 0;
  • REPULSIVE = 1;
  • HINGE = 2;
  • FIXED = 3;
FUNCTIONS
  • shape (type, center, mass, width, height, options) // use mass = 0 for fixed shapes
  • anchor (shape, position)
  • joint (type, shape1, anchor1, shape2, anchor2, strength, length)
  • transform (shape, positionOffset, angleOffset)
  • run(): run simulation
  • 2D vectors (shape center, anchor position, positionOffset) are arrays of the form [x, y].
  • Shape options: {f: friction, r: restitution, a: angle, v: velocity, g: gravity, d: data (color) ...}
  • The renderer is not included in the release, but an example is available below ↓


Demo


The "micro" version can only do the first two blocks below (see micro demo here).
Click the demo below to restart it.







Examples of user code


Simple renderer and game loop

// Canvas setup
c = a.getContext`2d`

// Draw
draw = (e, r) => {
  
  // reset canvas
  a.width ^= 0;
  
  // draw shapes
  for(e of H){
    
    c.save(),
    c.beginPath();
    
    // circle
    if(e.t === CIRCLE){
      c.fillStyle=e.d||"#bbb",
      c.translate(e.c[0],e.c[1]),
      c.rotate(e.a),
      c.arc(0,0,e.w,0,7),
      c.lineTo(0,0)
    }
    
    // rectangle
    else {
      c.fillStyle=e.d||"#bbb",
      c.moveTo(e.V[0][0],e.V[0][1]),
      c.lineTo(e.V[1][0],e.V[1][1]),
      c.lineTo(e.V[2][0],e.V[2][1]),
      c.lineTo(e.V[3][0],e.V[3][1])
    }
    
    c.closePath(),
    c.fill(),
    c.stroke()
    c.restore();
  
    // anchors
    for(r of e.p){
      c.beginPath(),
      c.fillStyle="#080",
      c.arc(r[0],r[1],5,0,7),
      c.fill(),
      c.closePath()
    }
  }
  
  // joints
  for(e of J){
    c.beginPath(),
    c.strokeStyle="#fa0",
    c.moveTo(e.A.p[e.a][0],e.A.p[e.a][1]),
    c.lineTo(e.B.p[e.b][0],e.B.p[e.b][1]),
    c.stroke(),
    c.closePath()
  }
}

// Game loop
setInterval(() => {
  time++;
  run();
  draw();
}, 16);

Shapes creation

// Simple rectangle
shape(RECTANGLE, [100, 50], 10, 20, 30); // type, center, mass, width, height

// Rectangle with all optional params
shape(
  RECTANGLE,    // type (0)
  [100, 50],    // center
  0,            // mass (0 = fixed)
  20,           // width
  30,           // height
  {             // options
    f: 0.9,     // friction (0-1, default: 0.5)
    r: 0.1,     // restitution (0-1, default: 0.5)
    a: .5,      // angle (in radians, default: 0)
    v: [2, 0],  // velocity (default: [0,0])
    A: .5,      // angular velocity (default: 0)
    g: [0, -2], // gravity (default: G)
    R: 0,       // enable rotations (default: 1)
    d: "red"    // custom data (ex: color)
  }
);

// Simple circle
shape(CIRCLE, [200, 50], 10, 50); // type, center, mass, radius

// Circle with all optional params
shape(
  CIRCLE,       // type (1)
  [200, 50],    // center
  10,           // mass
  50,           // radius
  50,           // radius again
  {             // options
    f: 0.9,     // friction (0-1, default: 0.5)
    r: 0.1,     // restitution (0-1, default: 0.5)
    a: .5,      // angle (in radians, default: 0)
    v: [2, 0],  // velocity (default: [0,0])
    A: .5,      // angular velocity (default: 0)
    g: [0, -2], // gravity (default: G)
    R: 0,       // enable rotations (default: 1)
    d: "red"    // custom data (ex: color)
  }
);

Spring joint

r1 = shape(RECTANGLE, [130, 100], 0, 90, 50);
c1 = shape(CIRCLE, [90, 250], 1, 30, 30);

// attach anchor a1 to r1 at local position [20, 15]
a1 = anchor(r1, [20,15]);

// attach anchor a2 to c1 at local position [0, -10]
a2 = anchor(c1, [0,-10]);

// Create spring joint between r1's anchor a1 and c1's anchor a2 with a .2 force and a 80px rest length
joint(SPRING, r1, a1, c1, a2, .2, 80);

Repulsive joint

r1 = shape(RECTANGLE, [100, 120], 0, 90, 50);
c1 = shape(CIRCLE, [120, 80], 1, 30);
a1 = anchor(r1, [0,-25]);
a2 = anchor(c1, [-20,0]);

// Create repulsive joint between r1's anchor a1 and c1's anchor a2 with a .2 force and a 200px min length
joint(REPULSIVE, r1, a1, c1, a2, .2, 200);

Fixed joint

r1 = shape(RECTANGLE, [100, 120], 10, 90, 50);
c1 = shape(CIRCLE, [100, 100], 10, 30);
a1 = anchor(r1, [0,-15]);
a2 = anchor(c1, [0,20]);

// Create fixed joint between r1's anchor a1 and c1's anchor a2
joint(FIXED, r1, a1, c1, a2);

Hinge joint

r1 = shape(HINGE, [130, 160], 0, 90, 50);
c1 = shape(CIRCLE, [165, 165], 100, 30, 30);
a1 = anchor(r1, [20,15]);
a2 = anchor(c1, [-20,10]);

// Create hinge joint between r1's anchor a1 and c1's anchor a2
joint(HINGE, r1, a1, c1, a2);

Kinematic shape

// Create a fixed shape
r1 = shape(RECTANGLE, [150, 100], 0, 50, 10);

// Update the shape's position and angle in the main loop

setInterval(() => {
  run();
  
  // At each frame, increment X position and decrement angle
  transform(r9, [1, 0], -0.01);
  
  draw();
}, 16);


© Public domain - Xem, 2025
Making-of - Source code on Github