使用方法
// 为何使用大括号包裹所有代码:就是一个语句块,去掉也行。用这个的好处是里面使用let,const定义的变量或常量,外面不可用,限制最小作用域。
{
const Human = class {
constructor(size, gravity, x, y, struct) {
this.points = [];
this.constraints = [];
this.shapes = [];
for (const point of struct.points) {
this.points.push(
new Human.Point(x, y, point, size, gravity)
);
}
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
)
);
}
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() {
for (const point of this.points) point.integrate();
for (const constraint of this.constraints) constraint.update();
for (const shape of this.shapes) shape.draw();
}
};
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() {
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.g * dir;
this.y += this.vy + 1.0 * (Math.random() - 0.5);
}
};
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.offset = shape.offset;
this.shape = document.createElement("canvas");
this.shape.width = this.height + this.width * this.offset;
this.shape.height = this.width;
const image = new Image();
image.onload = e => {
const ctx = this.shape.getContext("2d");
ctx.drawImage(image, 0, 0, this.height + this.width * this.offset, this.width);
};
image.src = "data:image/svg+xml;base64," + window.btoa(svg);
}
draw() {
const a = Math.atan2(this.p1.y - this.p0.y, this.p1.x - this.p0.x);
ctx.translate(this.p0.x, this.p0.y);
ctx.rotate(a);
ctx.drawImage(
this.shape,
-this.height * this.offset,
-this.width * 0.5
);
ctx.rotate(-a);
ctx.translate(-this.p0.x, -this.p0.y);
}
};
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);
}
};
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() {
const e = this.a12(this.p0, this.p1, this.p2);
this.a23(e, this.p1, this.p2);
}
};
const canvas = {
init() {
this.elem = document.createElement("canvas");
document.body.appendChild(this.elem);
this.resize();
window.addEventListener("resize", () => canvas.resize(), false);
const ctx = this.elem.getContext("2d");
if (!ctx.setLineDash) {
ctx.setLineDash = function () {}
}
return ctx;
},
resize() {
this.width = this.elem.width = this.elem.offsetWidth;
this.height = this.elem.height = this.elem.offsetHeight;
this.max = Math.max(canvas.width, canvas.height);
this.x = this.width * 0.5;
this.y = this.height * 0.5;
this.m = 100000;
this.background = document.createElement("canvas");
this.background.width = this.width;
this.background.height = this.height;
const ctx = this.background.getContext("2d");
let c = -50;
for (let i = canvas.max; i >= canvas.max / 10 - 1; i -= canvas.max / 5) {
ctx.beginPath();
ctx.arc(canvas.x, canvas.y, i, 0, 2 * Math.PI);
c += 30;
ctx.fillStyle = "hsl(208, 30%, " + c + "%)";
ctx.fill();
}
}
};
const pointer = {
init(canvas) {
this.x = 0;
this.y = 0;
this.pointDrag = null;
this.m = 10000;
this.msd = 0;
window.addEventListener("mousemove", e => this.move(e), false);
canvas.elem.addEventListener("touchmove", e => this.move(e), false);
window.addEventListener("mousedown", e => this.down(e), false);
window.addEventListener("touchstart", e => this.down(e), false);
window.addEventListener("mouseup", e => this.up(e), false);
window.addEventListener("touchend", e => this.up(e), false);
},
down(e) {
this.move(e);
this.msd = 1000000;
this.findPoint(human1);
this.findPoint(human2);
},
findPoint(human) {
for (const point of human.points) {
const dx = point.x - this.x;
const dy = point.y - this.y;
const sd = dx * dx + dy * dy;
if (sd < 5000 && sd < this.msd) {
this.msd = sd;
this.pointDrag = point;
}
}
},
drag() {
this.pointDrag.join(this, 0, 0.02);
ctx.beginPath();
ctx.lineWidth = 2;
ctx.setLineDash([2,2]);
ctx.moveTo(this.x, this.y);
ctx.lineTo(this.pointDrag.x, this.pointDrag.y);
ctx.stroke();
ctx.beginPath();
ctx.arc(this.x, this.y, 10, 0, 2 * Math.PI);
ctx.fill();
},
up(e) {
this.pointDrag = null;
},
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;
}
};
// ---- init ----
const ctx = canvas.init();
pointer.init(canvas);
let dir = 1;
// ---- main loop ----
const run = () => {
requestAnimationFrame(run);
ctx.drawImage(canvas.background, 0, 0);
if (Math.random() > 0.997) dir = -dir;
human1.points[16].join(human2.points[16], 0, 1);
human1.points[16].join(canvas, 0, 0.02);
human2.points[16].join(canvas, 0, 0.02);
human1.anim();
human2.anim();
if (pointer.pointDrag) pointer.drag();
};
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: [
{ 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 },
{
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.2, svg: "leg1", offset: 0.15 },
{ p0: 6, p1: 9, h: 3, w: 1.2, svg: "leg1", offset: 0.15 },
{ p0: 8, p1: 10, h: 4, w: 1.1, svg: "leg2", offset: 0.15 },
{ p0: 9, p1: 11, h: 4, w: 1.1, svg: "leg2", offset: 0.15 },
{ p0: 10, p1: 12, h: 0.5, w: 2.5, svg: "foo