{
// Code never lies, comments sometimes do.
const Human = class {
constructor(size, gravity, x, y, struct) {
this.x = x;
this.y = y;
this.points = [];
this.constraints = [];
this.shapes = [];
// Points
for (const point of struct.points) {
this.points.push(new Human.Point(canvas.width * x, canvas.height * y, point, size, gravity));
}
// Shapes
for (const shape of struct.shapes) {
this.shapes.push(
new Human.Shape(
this.points[shape.p0],
this.points[shape.p1],
shape,
struct.svg[shape.svg],
size
)
);
}
// constraints
for (const constraint of struct.constraints) {
if (constraint.angle) {
this.constraints.push(
new Human.Angle(
this.points[constraint.p0],
this.points[constraint.p1],
this.points[constraint.p2],
constraint
)
);
} else {
this.constraints.push(
new Human.Constraint(
this.points[constraint.p0],
this.points[constraint.p1],
constraint
)
);
}
}
}
anim() {
// Render human prototype
for (const point of this.points) point.integrate();
for (let i = 0; i < 5; ++i) {
for (const point of this.points) point.collide(ball);
for (const constraint of this.constraints) constraint.update();
}
for (const shape of this.shapes) shape.draw();
}
};
// Points
Human.Point = class {
constructor(x, y, p, s, g) {
this.x = x + p.x * s;
this.y = y + p.y * s;
this.px = this.x;
this.py = this.y;
this.vx = 0.0;
this.vy = 0.0;
this.m = p.m || 1.0;
this.g = g;
}
join(p1, distance, force) {
const dx = p1.x - this.x;
const dy = p1.y - this.y;
const dist = Math.sqrt(dx * dx + dy * dy);
const tw = this.m + p1.m;
const r1 = p1.m / tw;
const r0 = this.m / tw;
const dz = (distance - dist) * force;
const sx = dx / dist * dz;
const sy = dy / dist * dz;
p1.x += sx * r0;
p1.y += sy * r0;
this.x -= sx * r1;
this.y -= sy * r1;
}
dist(p1) {
const dx = this.x - p1.x;
const dy = this.y - p1.y;
return Math.sqrt(dx * dx + dy * dy);
}
integrate() {
// verlet integration
this.vx = this.x - this.px;
this.vy = this.y - this.py;
this.px = this.x;
this.py = this.y;
this.x += this.vx;
this.y += this.vy + this.g;
}
collide(ball) {
// collision with ball
const dx = this.x - ball.x;
const dy = this.y - ball.y;
const sd = dx * dx + dy * dy;
if (sd < ball.radius * ball.radius) {
const d = Math.sqrt(sd);
const dz = (ball.radius - d) * 0.5;
this.x += dx / d * dz;
this.y += dy / d * dz;
}
}
};
// Shapes
Human.Shape = class {
constructor(p0, p1, shape, svg, size) {
this.p0 = p0;
this.p1 = p1;
this.width = shape.w * size;
this.height = shape.h * size;
this.shape = canvas.createImage(
"data:image/svg+xml;base64," + window.btoa(svg)
);
this.offset = shape.offset;
}
draw() {
canvas.drawImage(
this.shape,
this.p0.x,
this.p0.y,
this.height + this.width * this.offset,
this.width, -this.height * this.offset, -this.width * 0.5,
Math.atan2(this.p1.y - this.p0.y, this.p1.x - this.p0.x)
);
}
};
// Constraints
Human.Constraint = class {
constructor(p0, p1, constraint) {
this.p0 = p0;
this.p1 = p1;
this.distance = p0.dist(p1);
this.force = constraint.force || 1.0;
}
update() {
this.p0.join(this.p1, this.distance, this.force);
}
};
// Angles constraints
Human.Angle = class {
constructor(p0, p1, p2, constraint) {
this.p0 = p0;
this.p1 = p1;
this.p2 = p2;
this.len1 = p0.dist(p1);
this.len2 = p1.dist(p2);
this.angle = constraint.angle.value;
this.range = constraint.angle.range;
this.force = constraint.force || 0.1;
}
a12(p0, p1, p2) {
const a = Math.atan2(p1.y - p0.y, p1.x - p0.x);
const b = Math.atan2(p2.y - p1.y, p2.x - p1.x);
const c = this.angle - (b - a);
const d = c > Math.PI ? c - 2 * Math.PI : c < -Math.PI ? c + 2 * Math.PI : c;
const e = Math.abs(d) > this.range ? (-Math.sign(d) * this.range + d) * this.force : 0;
const m = p0.m + p1.m;
const m1 = p0.m / m;
const m2 = p1.m / m;
const cos = Math.cos(a - e);
const sin = Math.sin(a - e);
const x1 = p0.x + (p1.x - p0.x) * m2;
const y1 = p0.y + (p1.y - p0.y) * m2;
p0.x = x1 - cos * this.len1 * m2;
p0.y = y1 - sin * this.len1 * m2;
p1.x = x1 + cos * this.len1 * m1;
p1.y = y1 + sin * this.len1 * m1;
return e;
}
a23(e, p1, p2) {
const a = Math.atan2(p1.y - p2.y, p1.x - p2.x) + e;
const m = p1.m + p2.m;
const m2 = p1.m / m;
const m3 = p2.m / m;
const cos = Math.cos(a);
const sin = Math.sin(a);
const x1 = p2.x + (p1.x - p2.x) * m2;
const y1 = p2.y + (p1.y - p2.y) * m2;
p2.x = x1 - cos * this.len2 * m2;
p2.y = y1 - sin * this.len2 * m2;
p1.x = x1 + cos * this.len2 * m3;
p1.y = y1 + sin * this.len2 * m3;
}
update() {
// resolve angular constraints
const e = this.a12(this.p0, this.p1, this.p2);
this.a23(e, this.p1, this.p2);
}
};
const canvas = {
// @greggman thanks for webglfundamentals!
// https://webglfundamentals.org/webgl/lessons/webgl-2d-drawimage.html
init() {
// create webGL canvas context
this.elem = document.createElement("canvas");
document.body.appendChild(this.elem);
const options = {
alpha: false,
stencil: false,
antialias: false,
depth: false
};
let gl = this.elem.getContext("webgl", options);
if (!gl) gl = this.elem.getContext("experimental-webgl", options);
if (!gl) return false;
this.gl = gl;
// set shaders
this.vertexShader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(
this.vertexShader,
`
precision highp float;
attribute vec4 aPosition;
uniform mat4 uMatrix;
varying vec2 vTexcoord;
void main() {
gl_Position = uMatrix * aPosition;
vTexcoord = aPosition.xy;
}
`
);
gl.compileShader(this.vertexShader);
this.fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(
this.fragmentShader,
`
precision highp float;
varying vec2 vTexcoord;
uniform sampler2D texture;
void main() {
gl_FragColor = texture2D(texture, vTexcoord);
}
`
);
// compile shaders
gl.compileShader(this.fragmentShader);
this.program = gl.createProgram();
gl.attachShader(this.program, this.vertexShader);
gl.attachShader(this.program, this.fragmentShader);
gl.linkProgram(this.program);
// init attributes, uniforms and buffers
this.position = gl.getAttribLocation(this.program, "aPosition");
gl.enableVertexAttribArray(this.position);
this.umatrix = gl.getUniformLocation(this.program, "uMatrix");
this.positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, this.positionBuffer);
gl.bufferData(
gl.ARRAY_BUFFER,
new Float32Array([0, 0, 0, 1, 1, 0, 1, 0, 0, 1, 1, 1]),
gl.STATIC_DRAW
);
gl.vertexAttribPointer(this.position, 2, gl.FLOAT, false, 0, 0);
// position/orientation/scaling matrix
this.matrix = new Float32Array(16);
this.matrix[15] = 1;
// source over blending
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
gl.enable(gl.BLEND);
gl.disable(gl.DEPTH_TEST);
gl.useProgram(this.program);
// resize event
this.resize();
window.addEventListener("resize", () => this.resize(), false);
return gl;
},
createTexture(source) {
// create gpu texture
const texture = this.gl.createTexture();
this.gl.bindTexture(this.gl.TEXTURE_2D, texture);
// assume non power of 2
this.gl.texParameteri(
this.gl.TEXTURE_2D,
this.gl.TEXTURE_WRAP_S,
this.gl.CLAMP_TO_EDGE
);
this.gl.texParameteri(
this.gl.TEXTURE_2D,
this.gl.TEXTURE_WRAP_T,
this.gl.CLAMP_TO_EDGE
);
this.gl.texParameteri(
this.gl.TEXTURE_2D,
this.gl.TEXTURE_MIN_FILTER,
this.gl.LINEAR
);
// upload texture
this.gl.texImage2D(
this.gl.TEXTURE_2D,
0,
this.gl.RGBA,
this.gl.RGBA,
this.gl.UNSIGNED_BYTE,
source
);
return texture;
},
createImageFromCamvas(canvas) {
// create texture from canvas
const img = {
width: canvas.width,
height: canvas.height,
texture: this.createTexture(canvas),
loaded: true
};
return img;
},
createImage(src) {
// create texture from URL (including svg converted to base64 URL)
const img = {
loaded: false
};
const source = new Image();
source.onload = e => {
img.loaded = true;
img.width = source.width;
img.height = source.height;
img.texture = this.createTexture(source);
};
source.src = src;
return img;
},
drawImage(
img,
x,
y,
width = img.width,
height = img.height,
offsetX = 0,
offsetY = 0,
angle = 0
) {
if (!img.loaded) return;
// update 4x4 matrix
const m = this.matrix;
const sx = 2 / this.width;
const sy = -2 / this.height;
if (angle) {
const c = Math.cos(angle);
const s = Math.sin(angle);
m[0] = c * sx * width;
m[1] = s * sy * width;
m[4] = -s * sx * height;
m[5] = c * sy * height;
m[12] = (c * offsetX - s * offsetY + x) * sx - 1;
m[13] = (s * offsetX + c * offsetY + y) * sy + 1;
} else {
m[0] = sx * width;
m[1] = 0;
m[4] = 0;
m[5] = sy * height;
m[12] = (offsetX + x) * sx - 1;
m[13] = (offsetY + y) * sy + 1;
}
this.gl.uniformMatrix4fv(this.umatrix, false, m);
// bind texture and draw the quad
this.gl.bindTexture(this.gl.TEXTURE_2D, img.texture);
this.gl.drawArrays(this.gl.TRIANGLES, 0, 6);
},
resize() {
this.width = this.elem.width = this.elem.offsetWidth;
this.height = this.elem.height = this.elem.offsetHeight;
// set viewport
this.gl.viewport(
0,
0,
this.gl.drawingBufferWidth,
this.gl.drawingBufferHeight
);
}
};
const pointer = {
init(canvas) {
this.x = canvas.width * 0.5;
this.y = canvas.height * 0.75;
this.ex = this.x;
this.ey = canvas.height * 2;
this.isDown = false;
window.addEventListener("mousemove", e => this.move(e), false);
canvas.elem.addEventListener("touchmove", e => this.move(e), false);
window.addEventListener("mousedown", e => this.isDown = true, false);
window.addEventListener("mouseup", e => this.isDown = false, false);
},
move(e) {
let touchMode = e.targetTouches,
pointer;
if (touchMode) {
e.preventDefault();
pointer = touchMode[0];
} else pointer = e;
this.x = pointer.clientX;
this.y = pointer.clientY;
},
ease(n) {
this.ex += (this.x - this.ex) * n;
this.ey += (this.y - this.ey) * n;
}
};
// init canvas (webGL)
const gl = canvas.init();
pointer.init(canvas);
// main loop
const run = () => {
requestAnimationFrame(run);
// clear screen
gl.clearColor(0.92, 0.68, 0, 1);
gl.clear(gl.COLOR_BUFFER_BIT);
// ease pointer
pointer.ease(0.33);
// draw ball
ball.x = pointer.ex;
ball.y = pointer.ey;
canvas.drawImage(ball, ball.x - ball.radius, ball.y - ball.radius);
// animate humans
for (const human of humans) {
human.anim();
hook.x = human.x * canvas.width;
hook.y = human.y * canvas.height;
human.points[16].join(hook, 0, 1);
if (pointer.isDown) human.points[16].join(ball, ball.radius, 1);
}
};
// create ball
const createBall = () => {
const ball = document.createElement("canvas");
const radius = 0.3 * Math.min(canvas.width, canvas.height);
ball.width = ball.height = 2 * radius;
const ctx = ball.getContext("2d");
ctx.beginPath();
ctx.arc(radius, radius, radius * 0.99, 0, 2 * Math.PI);
ctx.fillStyle = "#222";
ctx.fill();
const shape = canvas.createImageFromCamvas(ball);
shape.radius = radius;
shape.m = 1000;
return shape;
}
// create humans
const createHumans = () => {
const humans = [];
const s = canvas.height * 0.075;
const n = 0.25 * canvas.width / s;
for (let i = 0; i < n; ++i) {
humans.push(
new Human(
s,
0.2,
i / n,
0,
human
)
);
}
return humans;
}
// human ragdoll definition
const human = {
points: [{
x: 0,
y: 0
}, {
x: 0,
y: -2.8
}, {
x: 0,
y: -4
}, {
x: -1.1,
y: -2.2
}, {
x: 1.1,
y: -2.2
}, {
x: -0.5,
y: 1.2
}, {
x: 0.5,
y: 1.2
}, {
x: 0,
y: 1.5
}, {
x: -0.5,
y: 3.5
}, {
x: 0.5,
y: 3.5
}, {
x: -0.5,
y: 6.5
}, {
x: 0.5,
y: 6.5
}, {
x: -0.5,
y: 7
}, {
x: 0.5,
y: 7
}, {
x: 3,
y: -2.2
}, {
x: -3,
y: -2.2
}, {
x: 4.7,
y: -2.2
}, {
x: -4.7,
y: -2.2
}],
constraints: [
// distance constraints
{
p0: 0,
p1: 3
}, {
p0: 0,
p1: 4
}, {
p0: 1,
p1: 3
}, {
p0: 1,
p1: 4
}, {
p0: 3,
p1: 4
}, {
p0: 0,
p1: 5
}, {
p0: 0,
p1: 6
}, {
p0: 7,
p1: 5
}, {
p0: 7,
p1: 6
}, {
p0: 5,
p1: 6
},
// angular constraints
{
p0: 2,
p1: 1,
p2: 0,
angle: {
value: 0,
range: 1
}
}, {
p0: 3,
p1: 15,
p2: 17,
angle: {
value: -Math.PI / 2,
range: Math.PI / 3
}
}, {
p0: 4,
p1: 14,
p2: 16,
angle: {
value: -Math.PI / 2,
range: Math.PI / 3
}
}, {
p0: 1,
p1: 0,
p2: 7,
angle: {
value: 0,
range: 0.3
}
}, {
p0: 0,
p1: 5,
p2: 8,
angle: {
value: 0,
range: Math.PI / 3
}
}, {
p0: 0,
p1: 6,
p2: 9,
angle: {
value: 0,
range: Math.PI / 3
}
}, {
p0: 5,
p1: 8,
p2: 10,
angle: {
value: Math.PI / 2,
range: Math.PI / 3
}
}, {
p0: 6,
p1: 9,
p2: 11,
angle: {
value: Math.PI / 2,
range: Math.PI / 3
}
}, {
p0: 8,
p1: 10,
p2: 12,
angle: {
value: 0,
range: 0.2
}
}, {
p0: 9,
p1: 11,
p2: 13,
angle: {
value: 0,
range: 0.2
}
}
],
shapes: [{
p0: 0,
p1: 1,
h: 3,
w: 3.2,
svg: "tors",
offset: 0.35
}, {
p0: 1,
p1: 2,
h: 2,
w: 1.8,
svg: "head",
offset: 0.15
}, {
p0: 7,
p1: 0,
h: 2,
w: 2.8,
svg: "stomach",
offset: 0.1
}, {
p0: 5,
p1: 8,
h: 3,
w: 1.3,
svg: "leg1",
offset: 0.15
}, {
p0: 6,
p1: 9,
h: 3,
w: 1.3,
svg: "leg1",
offset: 0.15
}, {
p0: 8,
p1: 10,
h: 4,
w: 1.2,
svg: "leg2",
offset: 0.15
}, {
p0: 9,
p1: 11,
h: 4,
w: 1.2,
svg: "leg2",
offset: 0.15
}, {
p0: 10,
p1: 12,
h: 0.5,
w: 2.5,
svg: "foot",
offset: 0.3
}, {
p0: 11,
p1: 13,
h: 0.5,
w: 2.5,
svg: "foot",
offset: 0.3
}, {
p0: 4,
p1: 14,
h: 2.4,
w: 1,
svg: "arm1",
offset: 0.15
}, {
p0: 3,
p1: 15,
h: 2.4,
w: 1,
svg: "arm1",
offset: 0.15
}, {
p0: 14,
p1: 16,
h: 2.5,
w: 0.8,
svg: "arm2",
offset: 0.1
}, {
p0: 15,
p1: 17,
h: 2.5,
w: 0.8,
svg: "arm2",
offset: 0.1
}],
svg: {
// human shapes credits @roxik http://roxik.com/v/5/
head: `
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="100" height="70">
<g transform="scale(2 2) rotate(90) translate(17 -7)">
<path style="fill:#fff" d="M-9.9,-38.2Q-6.9 -40.7 -4.35 -39Q-2.3 -40.9 1.85 -41.8Q6.6 -42.85 10.05 -41.25Q12.3 -40.2 14.5 -38Q17 -37.2 18.05 -38.75Q17.85 -36.85 16.6 -35.5L18.4 -32.35Q17.6 -32.5 15.6 -34.25Q15.6 -32.6 17.9 -30L16.15 -30.75Q16.55 -27.2 15.3 -23.35Q14.75 -21.65 16.3 -19.3Q18.05 -16.7 18.05 -16L16.45 -15.1Q15.35 -14.65 14.4 -15.3Q13.65 -14.35 13.3 -10.95Q13 -8.1 11.4 -7.5Q10.25 -7 6.4 -7.5L6.8 -0.85Q7 2.05 5.3 4.05Q3.7 5.9 1.25 6.2Q-1.3 6.55 -3.35 5Q-5.65 3.3 -6.45 -0.25Q-5.5 -2.6 -5.85 -6.75L-6.7 -9.25L-8.55 -6.6Q-9.15 -7.85 -9.55 -10.55Q-9.85 -12.5 -9.95 -12.25Q-10.35 -10.75 -10.9 -10.3Q-10.3 -14.5 -10.95 -17.5L-12.5 -21.6Q-15 -26.45 -14.2 -30.1Q-13.05 -35.55 -9.9 -38.2" />
</g>
</svg>
`,
tors: `
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="100" height="100">
<g transform="rotate(90) translate(50 -22) scale(1.4 1.15)">
<path style="fill:#000" d="M30.8,-58.3Q34 -55.05 34 -50.5Q34 -47.2 29.5 -39.85Q25 -32.5 25 -31.5Q25 -28.05 23.5 -16.75Q22 -5.4 22 -3.5Q22 5.2 15.85 11.35Q9.7 17.5 1 17.5Q-7.7 17.5 -13.85 11.35Q-20 5.2 -20 -3.5Q-20 -5.4 -21.5 -16.75Q-23 -28.05 -23 -31.5Q-23 -32.5 -28.5 -39.8Q-34 -47.05 -34 -50.5Q-34 -55.05 -30.8 -58.3Q-27.55 -61.5 -23 -61.5Q-20.25 -61.5 -11.85 -64.5Q-3.45 -67.5 0 -67.5Q3.5 -67.5 12 -64.5Q20.5 -61.5 23 -61.5Q27.6 -61.5 30.8 -58.3" />
</g>
</svg>
`,
stomach: `
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="100" height="100">
<g transform="rotate(90) translate(50 -65) scale(1.5 1.5)">
<path style="fill:#000" d="M15.05,-13.35Q21.2 -7.2 21.2 1.5Q21.2 3.3 21.9 7.05L26.15 16.95Q27.6 21.3 25.45 20.8L24.5 20.55Q25.2 24.95 25.2 27Q25.2 33.1 21.45 37.25Q17.6 41.5 11.7 41.5Q3.65 41.5 -0.3 33.4Q-4.35 41.5 -12.3 41.5Q-18.15 41.5 -21.55 37.25Q-24.8 33.25 -24.8 27Q-24.8 24.55 -24.15 20.4L-25.5 20.8Q-27.75 21.3 -26 16.45L-21.3 5.65Q-20.8 2.65 -20.8 1.5Q-20.8 -7.2 -14.65 -13.35Q-8.5 -19.5 0.2 -19.5Q8.85 -19.5 15.05 -13.35" />
</g>
</svg>
`,
leg1: `
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="75" height="40">
<g transform="rotate(90) translate(20 -61) scale(1.5 1)">
<path style="fill:#000" d="M10.1,-6.8Q12.9 -2.6 12.55 2.95L12.25 48.65Q11.9 53.8 8.15 57.25Q4.35 60.65 -0.65 60.3Q-5.65 59.95 -8.95 56.05Q-12.2 52.1 -11.85 46.95L-12.55 1.15Q-12.2 -4.45 -8.6 -8.25Q-4.75 -12.35 0.95 -11.95Q6.9 -11.55 10.1 -6.8" />
</g>
</svg>
`,
leg2: `
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="80" height="40">
<g transform="rotate(-90) translate(-20 15) scale(1.5 1)">
<path style="fill:#000" d="M-9.25,-10.2Q-5.45 -14 -0.05 -14Q5.25 -14 8.65 -10.2Q11.95 -6.5 11.95 -1Q8.3 36.25 11.5 44.5Q14.35 51.85 12.95 52.7Q12.65 52.9 11.7 52.7Q10.95 52.55 10.95 52.95Q10 57.85 5.95 61.2Q2.6 64 -0.05 64Q-7.9 64 -11.05 52.95Q-12.3 53.55 -13 52.75Q-14.3 51.15 -11.35 44.25Q-7.95 36.35 -13.05 -1Q-13.05 -6.4 -9.25 -10.2" />
</g>
</svg>
`,
foot: `
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="50" height="50">
<g transform="rotate(-90) translate(-25 20) scale(0.88 1.5)">
<path style="fill:#fff" d="M8.25,-6.3Q11.45 -3.05 11.45 1.5L12.75 6.1Q14.1 7.8 18.85 9.75Q23.1 11.5 24.45 11.5Q26.15 11.5 26.55 14.5Q27 17.5 24.45 17.5L9.45 17.5Q8.1 17.5 5.5 15.75Q3.4 14.3 3.45 14.5Q3.6 15.05 3.25 16.15Q2.9 17.3 2.45 17.5L-9.55 17.5L-10.55 1.5Q-10.55 -3.05 -7.35 -6.3Q-4.1 -9.5 0.45 -9.5Q5.05 -9.5 8.25 -6.3" />
</g>
</svg>
`,
arm1: `
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="110" height="44">
<g transform="matrix(2, 0, 0, 2, 21, 21.5)">
<path style="fill:#000" d="M33.45,-6.4L35.65 -6.4Q39.7 -6.4 42.15 -4.3Q44.4 -2.4 44.5 0.3Q44.55 3 42.35 4.85Q39.95 6.95 35.6 6.95L34.6 7Q34.85 8.9 34.25 9.75Q33.2 8.9 29.35 8.55Q27.85 10.25 20.4 8.5L7.5 9.75L2.2 10.55Q-3.7 11.4 -7.2 8.25Q-10.3 5.45 -10.5 0.8Q-10.7 -3.75 -8 -7.1Q-5.05 -10.75 0 -10.75Q1.35 -10.75 4.05 -9.8Q7.1 -8.75 8.3 -8.6L21.65 -8L24.85 -8.9Q26.05 -9 28.6 -7.9Q32.8 -8 34.1 -8.6Q34.4 -7.45 33.45 -6.4" />
</g>
</svg>
`,
arm2: `
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="128" height="36">
<g transform="matrix(2, 0, 0, 2, 15.2, 18.5)">
<path style="fill:#fff" d="M38.65,-7.65L46.2 -9.25Q46.65 -7.6 44.7 -6.45L41.6 -5.15L48.95 -5.55Q54 -5.85 54.05 -5Q54.1 -3.7 45.35 -1.95L51.6 -2.2Q56.4 -2.25 56.4 -1.25Q56.4 -0.05 52 0.7L45.45 1.85L50.8 2.4Q54.35 2.7 54.3 3.5Q54.25 4.3 49.95 4.65L44.35 4.8Q46.75 5.05 48.6 6.1Q50.45 7.1 49.05 7.4L42.3 7.8Q36.35 7.85 33.4 5.9Q31.1 4.65 15.2 6.8Q-0.55 8.9 -4.3 6.7Q-8.6 4.15 -7.3 -0.75Q-6 -5.75 -0.75 -6.25Q2.35 -6.55 15.7 -4.45L31.2 -3.95Q33.4 -6.4 38.65 -7.65" />
</g>
</svg>
`
}
};
// LET THERE BE LIGHT!
const humans = createHumans();
const ball = createBall();
const hook = {
x: 0,
y: 0,
m: 100
};
requestAnimationFrame(run);
}
body,
html {
position: absolute;
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
}
canvas {
position: absolute;
width: 100%;
height: 100%;
cursor: none;
}
console