A 2D physics engine featuring emoji! Click to add an emoji, double-click to add a fixed circle. Video: https://twitter.com/MaximeEuziere/status/1092330191869931521
for(_='-1!o(@z(@O&&(Qa.widthUU/We(_=_qb.~,~$at#d.HHD*m.GV,@!FHpageLd,Ky(l,J--;)for(HtHbV(b,0,,~dq~d=C()**~):c.fillHd),c.=!.5*g(n,=f.length;)t,HD+=y(m,*2*t= *2t+y(m,M#h.random(69|0+12a.heightOLX,LY/(~t+~V~Vt$tHVqHVt,~D-=J=(Kb=W50+9,t=1/b)=>zf.push({KV:@0t,B:t?:D:Z:String.fromCodePoint(8512)}o({Kb}rg(Kd*.5,e@+~K+~bC_d!V@**bgd+yb-KRK1/(r(d)||1f=[];OW2,+W4W2,0i=99;iOW2+W4,-));onclicketail>1?,0):setInterval(\'U^=ii{jjb=f[i],d=f[j],M$dr(M)<~b+QD=~b+-r(MN=R(MSqR(M,!EqSN,D(~t||)Qh=N,D.8h,-~tqh,pqS,E$t)lp$dmp,n_HF*GGd_~FD*l.~D*l.d)sN)+JNN N,sNsNs*,u=R(C(nN,g(n,N))!xu.5+Juu u,xuxux*));~tQ~V.b++$B+=.02D,~V,.02)c.sav_beginP#h(transl#_~$rot#_~Barc(~7lineT@0font=11.5b+"% a",f[i].t?navig#or.userAgent.m#ch`Ma`?c.strok_Text(~Z,1.3*-~.65b(restor_)}\',16)';G=/[-J-LF-H#$~q_WUQO@!]/.exec(_);)with(_.split(G))_=join(shift());eval(_)
Zm9yKF89Jy0xIW8oQHooQE8mJihRYS53aWR0aFVVL1dlKF89X3FiLn4sfiRhdCNkLkhIRCptLkdWLEAhRkhwYWdlTGQsS3kobCxKLS07KR9mb3IoHkh0HUhiHFYoG2IsGjAsGSwbGH5kcX5kGBc9QygWKSoVKn4UKTpjLmZpbGwTSGQSKSwREWMuED0hLjUqZyhuLA89Zi5sZW5ndGg7DikRDBR0LEhEKz15KG0sCxUqMiodEXQ9GwkVKjIUdCt5KG0sCE0jaC5yYW5kb20oFQcHNjl8MCsxMgZhLmhlaWdodAVPTFgsTFkELyh+dCsdAxF+VhZ+Vhh0JHQMSFZxSFYYdCwdDH5ELT1KAj0oS2I9B1c1MCs5LHQ9MS9iKT0+AXoBZi5wdXNoKHtLVjpAGTARdCxCOnQ/BjoZRDoZGlo6U3RyaW5nLmZyb21Db2RlUG9pbnQoBjg1MTIpfRFvASh7S2J9EXIBZyhLZBUqLjUsZQFAEit+SxwrfmIRQwFfZBgaIQxWAUASKhocKmIRZwESFGQrHBQaeQESFGItHBRLUgEbSzEvKHIoZCl8fDEMZj1bXTseT1cyLAUrVzQRVzIsMBFpPTk5O2kfTwdXMitXNCwHBS0FKSk7b25jbGljawESZXRhaWw+MT8EEQYsMCk6BAxzZXRJbnRlcnZhbChcJx5VXj0ZaQ5pH3seag5qH2I9ZltpXSxkPWZbal0sTRYSJGQRcihNKTx+YiscUUQ9fmIrHC1yKE0RTj1SKE0RU3ESGFIoG00sIQwcDEVxUxhOLEQMKH50fHwdKVFoPRtOLEQDFS44ERdoLC1+dAwScRIYaCwdDHBxG1MsHQMMG0UkdAMpDGwWcCRkEW0WcCwSEW4WX0hGKkcaR2QMX35GFEQqbC4afkQqbC5kKQxzD04pAytKTghOCU4scwJOFXMLThVzKh0sdT0bUihDKG4YTixnKG4sTikpDCEReA91FS41AytKdQh1CXUseAJ1FXgLdRV4Kh0pKTt+dFF+Vi5iKyskQis9LjAyFEQsF35WLC4wMikMYy5zYXZfEGJlZ2luUCNoKBB0cmFuc2wjX34SJBwQcm90I19+QhBhcmMoGRl+Ghk3EGxpbmVUQBkwEGZvbnQ9MTEuNRRiKyIlIGEiLGZbaV0udD9uYXZpZyNvci51c2VyQWdlbnQubSNjaGBNYWA/Yy5zdHJva18TVGV4dCh+WiwxLjMqLX4aLjY1FGITKBByZXN0b3JfKX1cJywxNiknO0c9L1sBLR9KLUxGLUgjJH5xX1dVUU9AIV0vLmV4ZWMoXyk7KXdpdGgoXy5zcGxpdChHKSlfPWpvaW4oc2hpZnQoKSk7ZXZhbChfKQ==
// Circle generator
// ----------------
// Params:
// - C: center coordinates
// - R: radius (random between 10 and 10 + canvas width / 50)
// - M: inverseMass (1/R by default, or 0 if the circle is immobile)
// Note: here, for simplicity, the mass of a circle is equal to its radius (it can be any value).
// In 2D physics equations, the mass is almost never used as-is: it is usually inverted (1/mass).
// So it's a good practice to store and use the inverse mass directly, to avoid many useless divisions.
var Circle = (C, R = Math.random() * a.width/50 + 9, M = 1 / R) =>
// Add a new circle into `objects`, the list of circles in the scene:
objects.push({
C, // C: center
V: Vec2(0, 0), // V: velocity (speed)
M, // M: inverseMass
B: M ? Math.random() * 69 | 0 + 12 : 0, // B: angle (`*69|0+12` is used here instead of `*2*PI` to optimize compression)
D: 0, // D: angle velocity
R, // R: radius
Z: String.fromCodePoint( // Z: random emoji between U+1F600 and U+1F645
Math.random() * 0x45 | 0 + 0x1F600
)
// Note: in a realistic 2D engine, there should be an extra variable called Inertia, proportional to mass * radius².
// Here it's simply replaced by M to save space.
});
// 2D vector library
// -----------------
// These 8 functions are used to generate and manipulate 2D vectors in the form of objects {x, y}.
// A 2D vector can either represent:
// - a position on the scene (ex: the center of a circle)
// - a force (ex: a collision)
// - a magnitude (ex: a circle's velocity or acceleration)
// Note: all these functions have the same 3 parameters (x,y,t) even when only one or two are necessary.
// This is a RegPack optimization (the more a function signature is repeated, the better it's compressed).
// More info on codegolfctober 2018: http://xem.github.io/articles/#codegolfctober18 (days 3 & 4)
var Vec2 = (x,y) => ({x,y}); // Vec2 constructor
var length = (x,y)=> dot(x,x)**.5; // Vector length (Math.hypot(x.x, x.y))
var add = (x,y,t) => Vec2(x.x + y.x, x.y + y.y); // Add two vectors (x + y)
var subtract = (x,y) => add(x, scale(y, -1)); // Subtract two vectors (x - y)
var scale = (x,y) => Vec2(x.x * y, x.y * y); // Scale a vector (x * y)
var dot = (x,y) => x.x * y.x + x.y * y.y; // Dot product (x ∙ y)
var cross = (x,y) => x.x * y.y - x.y * y.x; // Cross product (x ⨯ y)
var normalize = (x,y) => scale(x, 1 / (length(x) || 1)); // Normalize a vector (‖x‖ sets length to 1 if it has a length)
// Objects list
// ------------
var objects = [];
// Scene initialization
// --------------------
// Create a big, immobile circle at the bottom of the screen
Circle(Vec2(a.width/2, a.height + a.width/4), a.width/2, 0);
// Create 99 circles with random size and position, above the top of the screen
for(i = 99; i--; ) Circle(Vec2(Math.random() * a.width/2 + a.width/4, Math.random() * a.height - a.height));
// Interactivity
// -------------
// Create a new mobile circle on click and an immobile one on double click
onclick = e => e.detail > 1 ? Circle(Vec2(d.pageX, d.pageY), Math.random() * 69 | 0 + 12, 0) : Circle(Vec2(d.pageX, d.pageY));
// Animation loop
// --------------
setInterval(
// Execute this function every 9ms:
e => {
// Reset the canvas
a.width ^= 0;
// Loop on all pairs of circles.
for(i = objects.length; i--;){
for(j = objects.length; j--;){
// Note: another for loop (ex: k = 0 to 15) could be added here for more precision.
// Such a loop would force the simulation to run 15 times per frame and ensure all the collisions are fully resolved.
// Though, it's not necessary for such a small demo: a single pass works fine.
// The inner loop could have been simplified with `;j-- > i;` but it would take more bytes.
// Call the two circles b and d.
b = objects[i];
d = objects[j];
// Test collisions
// ---------------
// The two circles collide if the distance between their centers is smaller than the sum of their radii.
e = subtract(d.C, b.C);
if(length(e) < b.R + d.R){
// Compute the collision's properties
D = b.R + d.R - length(e), // D: depth
N = normalize(e), // N: normal
S = add(d.C, scale(normalize(scale(e, -1)), d.R)), // S: start point
E = add(S, scale(N, D)) // E: end point
// Resolve collision
// -----------------
// If at least one of the circles is not immobile
if(b.M || d.M){
// First, correct the two circles' positions
// The two circles are moved away from each other along N (the collision's normal) and proportionally to their masses.
// The move distance is not equal to their interpenetration, but scaled down using a position correction rate (here, 0.8).
// This rate makes the resolution of multiple collisions on the same shape smoother.
h = scale(N, D / (b.M + d.M) * .8);
b.C = add(b.C, scale(h, -b.M));
d.C = add(d.C, scale(h, d.M));
// Then, compute the relative velocity between the two circles
p = add(scale(S, d.M / (b.M + d.M)), scale(E, b.M / (b.M + d.M)));
l = subtract(p, b.C);
m = subtract(p, d.C);
n = subtract(add(d.V, Vec2(-1 * d.D * m.y, d.D * m.x)), add(b.V, Vec2(-1 * b.D * l.y, b.D * l.x)));
// An optimization was skipped here to save bytes: `if(dot(n, N) < 0)`.
// It ensures that the circles actually move towards each other before resolving their collision.
// Restitution
// -----------
// Restitution makes the circles bounce on each other.
// It can have any value for each circle, but it is fixed in this demo: 0.5 for all circles.
// If both circles had a different restitution, the smallest one would apply.
// Compute jN.
s = (-1.5 * dot(n, N)) / (b.M + d.M + cross(l, N) ** 2 * b.M + cross(m, N) ** 2 * d.M);
// Compute impulse.
t = scale(N, s);
// Update the circles' velocity and angular velocity
b.V = subtract(b.V, scale(t, b.M));
d.V = add(d.V, scale(t, d.M));
b.D -= cross(l, N) * s * b.M;
d.D += cross(m, N) * s * d.M;
// Friction
// --------
// Friction makes the circles roll or glide against each other.
// It can have any value for each circle, but it is also fixed to 0.5.
// If both circles had a different friction, the smallest one would apply.
// Compute tangent.
u = scale(normalize(subtract(n, scale(N, dot(n, N)))), -1);
// Compute jT.
x = -1.5 * dot(n, u) * .5 / (b.M + d.M + cross(l, u) ** 2 * b.M + cross(m, u) ** 2 * d.M);
// compute angular impulse.
t = scale(u, x);
// Update the circles' velocity and angular velocity
b.V = subtract(b.V, scale(t, b.M));
d.V = add(d.V, scale(t, d.M));
b.D -= cross(l, u) * x * b.M;
d.D += cross(m, u) * x * d.M;
}
}
}
// At this point, we're in the first loop, so we will only update the circle called b.
// Update the scene
// ----------------
// If the circle is mobile:
// - add gravity to the circles' velocity: b.V += [0,1]
// - add angular velocity to the circle's angle
// - Add velocity to the circle's position
if(b.M){
b.V.y++
b.B += b.D * .02;
b.C = add(b.C, scale(b.V, .02));
}
// Draw
// ----
c.save();
c.beginPath();
// Translate and rotate the canvas' context to the circle's position and angle
c.translate(b.C.x, b.C.y);
c.rotate(b.B);
// Make an arc with the circle's radius
c.arc(0, 0, b.R, 0, 7);
// Make a line from the arc to the center
c.lineTo(0,0);
// Set the font size in % equal to the circle's radius * 11.5 (to make the emoji appear with the right radius)
c.font = b.R * 11.5 + "% a";
// If the circle is mobile:
if(objects[i].M ){
// On MacOS and iOS: stroke the circle and the line
// More info on this test: https://twitter.com/MaximeEuziere/status/1088808226790010880
if(navigator.userAgent.match("Ma")) c.stroke();
// On Linux, Windows and Android: draw the emoji. The offsets (R * -1.24 and R * .67) make the emoji appear at the right place.
else c.fillText(b.Z, -b.R * 1.3, b.R * .65);
}
// If the circle is immobile, fill the arc in black
else c.fill();
c.restore();
}
},
16
);
// After minification:
// - I replaced the function in setInterval(...) with a string.
// - I renamed `M()` as `z()`.
// - I reused the signature of Circle() for all the other functions: `(d,b=Math.random()*a.width/50+9,t=1/b)=>...`.
// The minified size is 1820b.
// After RegPacking (with the vars a,c,z ignored and 2/1/0 score), it fits in 1024b.