|
/* |
|
Artwork by Angelo Plessas | http://homonoosphericus.com/ |
|
Code by Gerard Ferrandez | https://codepen.io/ge1doot/pen/MzGPJY |
|
*/ |
|
"use strict"; |
|
{ |
|
const points = []; |
|
const constraints = []; |
|
const shapes = []; |
|
const kImgScale = 0.6; |
|
const kGravity = 0.1; |
|
const base = "https://s3-us-west-2.amazonaws.com/s.cdpn.io/222599/"; |
|
Math.sign = Math.sign || function(x) { |
|
return x > 0 ? 1 : -1; |
|
}; |
|
/////////////// Point Class ////////////////// |
|
const Point = class { |
|
constructor(p) { |
|
this.x = canvas.width * 0.5 + p.x * canvas.scale; |
|
this.y = p.y * canvas.scale; |
|
this.oldX = this.x; |
|
this.oldY = this.y; |
|
this.radius = (p.r || 1.0) * kImgScale * canvas.scale; |
|
this.mass = p.m === undefined ? 1.0 : p.m; |
|
this.gravity = p.g === undefined ? kGravity : p.g; |
|
this.static = p.static === undefined ? false : true; |
|
this.ground = p.ground === undefined ? false : true; |
|
this.audio = p.audio === undefined ? Math.floor(Math.random() * 5) : p.audio; |
|
this.pointer = pointer; |
|
this.canvas = canvas; |
|
} |
|
integrate(dt) { |
|
if (this.static === true) return; |
|
const x = this.x; |
|
const y = this.y; |
|
this.x += (this.x - this.oldX) + this.canvas.wind; |
|
this.y += (this.y - this.oldY) + this.gravity; |
|
this.oldX = x; |
|
this.oldY = y; |
|
if (this.ground === true) { |
|
if (this.y > this.canvas.height - this.radius) { |
|
this.x = x; |
|
this.y = this.canvas.height - this.radius; |
|
} |
|
} |
|
if (!this.pointer.draggable) { |
|
const dx = this.x - this.pointer.x; |
|
const dy = this.y - this.pointer.y; |
|
if (Math.sqrt(dx * dx + dy * dy) < this.radius) |
|
this.pointer.draggable = true; |
|
} |
|
} |
|
dist(p) { |
|
const dx = this.x - p.x; |
|
const dy = this.y - p.y; |
|
return Math.sqrt(dx * dx + dy * dy); |
|
} |
|
}; |
|
/////////////// Angle Constraint Class ////////////////// |
|
const AngleConstraint = class { |
|
constructor(struct, c) { |
|
this.p1 = struct.points[c.p1]; |
|
this.p2 = struct.points[c.p2]; |
|
this.p3 = struct.points[c.p3]; |
|
this.len1 = this.p1.dist(this.p2); |
|
this.len2 = this.p2.dist(this.p3); |
|
this.angle = c.a; |
|
this.range = c.r; |
|
this.force = c.f || 0.2; |
|
} |
|
// solve 2 vectors angled constraint |
|
// http://stackoverflow.com/questions/16336702/ragdoll-joint-angle-constraints |
|
solve() { |
|
let e = 0; |
|
{ |
|
const a = Math.atan2(this.p2.y - this.p1.y, this.p2.x - this.p1.x); |
|
const b = Math.atan2(this.p3.y - this.p2.y, this.p3.x - this.p2.x); |
|
const c = this.angle - (b - a); |
|
const d = c > Math.PI ? c - 2 * Math.PI : c < -Math.PI ? c + 2 * Math.PI : c; |
|
e = Math.abs(d) > this.range ? (-Math.sign(d) * this.range + d) * this.force : 0; |
|
const m = this.p1.mass + this.p2.mass; |
|
const m1 = this.p1.mass / m; |
|
const m2 = this.p2.mass / m; |
|
const cos = Math.cos(a - e); |
|
const sin = Math.sin(a - e); |
|
const x1 = this.p1.x + (this.p2.x - this.p1.x) * m2; |
|
const y1 = this.p1.y + (this.p2.y - this.p1.y) * m2; |
|
this.p1.x = x1 - cos * this.len1 * m2; |
|
this.p1.y = y1 - sin * this.len1 * m2; |
|
this.p2.x = x1 + cos * this.len1 * m1; |
|
this.p2.y = y1 + sin * this.len1 * m1; |
|
}{ |
|
const a = Math.atan2(this.p2.y - this.p3.y, this.p2.x - this.p3.x) + e; |
|
const m = this.p2.mass + this.p3.mass; |
|
const m2 = this.p2.mass / m; |
|
const m3 = this.p3.mass / m; |
|
const cos = Math.cos(a); |
|
const sin = Math.sin(a); |
|
const x1 = this.p3.x + (this.p2.x - this.p3.x) * m2; |
|
const y1 = this.p3.y + (this.p2.y - this.p3.y) * m2; |
|
this.p3.x = x1 - cos * this.len2 * m2; |
|
this.p3.y = y1 - sin * this.len2 * m2; |
|
this.p2.x = x1 + cos * this.len2 * m3; |
|
this.p2.y = y1 + sin * this.len2 * m3; |
|
} |
|
} |
|
}; |
|
/////////////// Constraint Class ////////////////// |
|
const Constraint = class { |
|
constructor(struct, c) { |
|
this.p1 = struct.points[c.p1]; |
|
this.p2 = struct.points[c.p2]; |
|
this.len = c.len === undefined ? this.p1.dist(this.p2) : c.len; |
|
this.force = c.f || 1.0; |
|
} |
|
solve() { |
|
const dx = this.p1.x - this.p2.x; |
|
const dy = this.p1.y - this.p2.y; |
|
const d = Math.sqrt(dx * dx + dy * dy); |
|
const tm = this.p1.mass + this.p2.mass; |
|
const d2 = (d - (d + (this.len - d) * this.force)) / d * 0.5; |
|
if (this.p1.static === false) { |
|
const s2 = d2 * (this.p2.mass / tm); |
|
this.p1.x -= (dx * s2); |
|
this.p1.y -= (dy * s2); |
|
} |
|
if (this.p2.static === false) { |
|
const s1 = d2 * (this.p1.mass / tm); |
|
this.p2.x += (dx * s1); |
|
this.p2.y += (dy * s1); |
|
} |
|
} |
|
}; |
|
/////////////// Shape Class ////////////////// |
|
const Shape = class { |
|
constructor(struct, i) { |
|
this.p1 = struct.points[i.p1]; |
|
this.p2 = struct.points[i.p2]; |
|
this.img = new Image(); |
|
this.img.src = document.getElementById(i.img) ? document.getElementById(i.img).src : base + i.img; |
|
this.ox = i.ox * kImgScale * canvas.scale; |
|
this.oy = i.oy * kImgScale * canvas.scale; |
|
this.w = i.w * kImgScale * canvas.scale; |
|
this.h = i.h * kImgScale * canvas.scale; |
|
this.angle = i.a * 1.0; |
|
this.loaded = false; |
|
this.ctx = ctx; |
|
} |
|
draw() { |
|
const a = Math.atan2(this.p2.y - this.p1.y, this.p2.x - this.p1.x) + this.angle; |
|
const cos = Math.cos(a); |
|
const sin = Math.sin(a); |
|
this.ctx.setTransform(cos, sin, -sin, cos, this.p1.x, this.p1.y); |
|
if (this.loaded) { |
|
this.ctx.drawImage(this.img, -this.ox - 1, -this.oy - 1); |
|
} else { |
|
if (this.img.complete) { |
|
this.loaded = true; |
|
let img; |
|
if (window.OffscreenCanvas) { |
|
img = new OffscreenCanvas(this.w + 2, this.h + 2); |
|
} else { |
|
img = document.createElement('canvas'); |
|
img.width = this.w + 2; |
|
img.height = this.h + 2; |
|
} |
|
img.getContext("2d").drawImage(this.img, 1, 1, this.w, this.h); |
|
this.img = img; |
|
} |
|
ctx.beginPath(); |
|
this.ctx.strokeStyle = "#333"; |
|
this.ctx.setLineDash([]); |
|
this.ctx.strokeRect(-this.ox, -this.oy, this.w, this.h); |
|
} |
|
} |
|
}; |
|
/////////////// Stroke Class ////////////////// |
|
const Stroke = class { |
|
constructor(struct, s) { |
|
this.p1 = struct.points[s.p1]; |
|
this.p2 = struct.points[s.p2]; |
|
this.color = s.c; |
|
} |
|
draw() { |
|
ctx.beginPath(); |
|
ctx.strokeStyle = this.color; |
|
ctx.lineWidth = 2; |
|
ctx.setLineDash([1, 1]); |
|
ctx.moveTo(this.p1.x, this.p1.y); |
|
ctx.lineTo(this.p2.x, this.p2.y); |
|
ctx.stroke(); |
|
} |
|
}; |
|
/////////////// Main Loop ////////////////// |
|
const run = () => { |
|
requestAnimationFrame(run); |
|
ctx.clearRect(0, 0, canvas.width, canvas.height); |
|
pointer.dragging(); |
|
ctx.save(); |
|
for (const s of shapes) s.draw(); |
|
ctx.restore(); |
|
for (const p of points) p.integrate(); |
|
for (const c of constraints) c.solve(); |
|
pointer.cursor(); |
|
canvas.tick(); |
|
} |
|
/////////////// canvas ////////////////// |
|
const canvas = { |
|
wind: 0, |
|
windf: 0, |
|
elem: document.createElement("canvas"), |
|
resize: function(struct) { |
|
this.width = this.elem.width = this.elem.offsetWidth; |
|
this.height = this.elem.height = this.elem.offsetHeight; |
|
this.scale = canvas.height / 900; |
|
if (struct) { |
|
struct.points.mong30.x = canvas.width * 0.5 - canvas.width * 0.25 * this.scale; |
|
struct.points.mong30.y = this.height; |
|
struct.points.mong10.x = this.width * 0.5; |
|
struct.points.mong20.x = this.width * 0.5; |
|
} |
|
}, |
|
init: function() { |
|
const ctx = canvas.elem.getContext("2d", {lowLatency: true, alpha: false}); |
|
if (!ctx.setLineDash) ctx.setLineDash = function () {}; |
|
document.body.appendChild(canvas.elem); |
|
window.addEventListener("resize", e => this.resize(struct), false); |
|
this.resize(); |
|
return ctx; |
|
}, |
|
tick() { |
|
this.windf += 0.005; |
|
this.wind = 0.05 * Math.sin(this.windf * 0.5) * Math.cos(this.windf); |
|
} |
|
}; |
|
const ctx = canvas.init(); |
|
/////////////// Pointer ////////////////// |
|
const pointer = { |
|
x: 0, |
|
y: 0, |
|
drag: null, |
|
draggable: false, |
|
cursor: function() { |
|
canvas.elem.className = this.drag |
|
? "dragging" |
|
: this.draggable ? "draggable" : "default"; |
|
}, |
|
dragging: function() { |
|
this.draggable = false; |
|
if (pointer.drag) { |
|
this.drag.x += (this.x - this.drag.x) / 20; |
|
this.drag.y += (this.y - this.drag.y) / 20; |
|
} |
|
}, |
|
pointer: function(e) { |
|
let pointer; |
|
if (e.targetTouches) { |
|
e.preventDefault(); |
|
pointer = e.targetTouches[0]; |
|
} else pointer = e; |
|
return pointer; |
|
}, |
|
addEvents: function() { |
|
[ |
|
[ |
|
window, |
|
"mousemove,touchmove", |
|
function(e) { |
|
const pointer = this.pointer(e); |
|
this.x = pointer.clientX; |
|
this.y = pointer.clientY; |
|
} |
|
], |
|
[ |
|
canvas.elem, |
|
"mousedown,touchstart", |
|
function(e) { |
|
if (!this.drag) { |
|
const pointer = this.pointer(e); |
|
this.x = pointer.clientX; |
|
this.y = pointer.clientY; |
|
let dm = 9999; |
|
for(const p of points) { |
|
const dx = p.x - this.x; |
|
const dy = p.y - this.y; |
|
const d = Math.sqrt(dx * dx + dy * dy); |
|
if (d < p.radius * 2) { |
|
if (d < dm) { |
|
dm = d; |
|
this.drag = p; |
|
} |
|
} |
|
} |
|
if (this.drag) { |
|
if (audio !== null) audio[this.drag.audio].play(); |
|
} |
|
} |
|
} |
|
], |
|
[ |
|
window, |
|
"mouseup,touchend,touchcancel", |
|
function() { |
|
this.drag = null; |
|
} |
|
] |
|
].forEach( |
|
function(e) { |
|
for (let i = 0, events = e[1].split(","); i < events.length; i++) { |
|
e[0].addEventListener(events[i], e[2].bind(this), false); |
|
} |
|
}.bind(this) |
|
); |
|
} |
|
}; |
|
pointer.addEvents(); |
|
/////////////// Homo Noosphericus structure ////////////////// |
|
const struct = { |
|
points: { |
|
p0: {x: -45, y: 45, r: 30, m: 1}, |
|
p1: {x: 0, y: -75, r: 135, m: 1, ground: true}, |
|
p2: {x: 0, y: 290, r: 20, m: 1}, |
|
p3: {x: -35, y: 280, r: 30, m: 1, ground: true}, |
|
p4: {x: 50, y: 45, r: 30, m: 1}, |
|
p5: {x: 20, y: 280, r: 30, m: 1, ground: true}, |
|
p6: {x: 50, y: 430, r: 20, m: 0.5, g: -0.06, ground: true}, |
|
p7: {x: -46, y: 430, r: 20, m: 0.5, g: -0.06, ground: true}, |
|
p8: {x: 50, y: 660, r: 40, m: 0.35, ground: true}, |
|
p9: {x: -46, y: 660, r: 40, m: 0.35, ground: true}, |
|
p10: {x: -187, y: 100, r: 30, m: 0.5}, |
|
p11: {x: 187, y: 100, r: 30, m: 0.5}, |
|
p12: {x: -380, y: 115, r: 60, m: 0.35, g: -0.02}, |
|
p13: {x: 380, y: 115, r: 60, m: 0.35, g: -0.02}, |
|
p14: {x: 0, y: 90, r: 50, m: 30, g: 0.2}, |
|
p15: {x: 0, y: 140, r: 0}, |
|
p16: {x: 0, y: -110, r: 4, m: 0.1, g: 0}, |
|
mong10: {x: 0, y: -200, static: true}, |
|
mong11: {x: -350, y: 0, r: 130, m: 10}, |
|
mong12: {x: -350, y: 10, r: 0, m: 0, g: 0}, |
|
mong20: {x: 0, y: -200, static: true}, |
|
mong21: {x: 350, y: 0, r: 130, m: 10}, |
|
mong22: {x: 350, y: 100, r: 0, m: 0, g: 0}, |
|
mong30: {x: -canvas.width * 0.25 * canvas.scale, y: 1 / canvas.scale * canvas.height, r: 0, static: true}, |
|
mong31: {x: 0, y: 1 / canvas.scale * canvas.height * 0.6, r: 250, m: 10, g: -0.03, audio: 5}, |
|
mong32: {x: 0, y: 1 / canvas.scale * canvas.height * 0.6 - 100, r: 1, m: 0, g: 0}, |
|
mong13: {x: -350, y: 300, r: 50, m: 1}, |
|
mong14: {x: -350, y: 350, r: 50, m: 0.1}, |
|
mong15: {x: -350, y: 400, r: 50, m: 1}, |
|
mong16: {x: -350, y: 450, r: 50, m: 0.1}, |
|
mong23: {x: 350, y: 300, r: 50, m: 1}, |
|
mong24: {x: 350, y: 350, r: 50, m: 0.1}, |
|
mong25: {x: 350, y: 400, r: 50, m: 1}, |
|
mong26: {x: 350, y: 450, r: 50, m: 0.1} |
|
}, |
|
constraints: [ |
|
{p1: "p0", p2: "p1"}, |
|
{p1: "p1", p2: "p2"}, |
|
{p1: "p2", p2: "p3"}, |
|
{p1: "p0", p2: "p2"}, |
|
{p1: "p1", p2: "p3"}, |
|
{p1: "p1", p2: "p4"}, |
|
{p1: "p5", p2: "p2"}, |
|
{p1: "p1", p2: "p5"}, |
|
{p1: "p2", p2: "p4"}, |
|
{p1: "p0", p2: "p4"}, |
|
{p1: "p3", p2: "p5"}, |
|
{p1: "p3", p2: "p4"}, |
|
{p1: "p0", p2: "p10"}, |
|
{p1: "p4", p2: "p11"}, |
|
{p1: "p10", p2: "p12"}, |
|
{p1: "p11", p2: "p13"}, |
|
{p1: "p14", p2: "p3"}, |
|
{p1: "p14", p2: "p0"}, |
|
{p1: "p14", p2: "p4"}, |
|
{p1: "p14", p2: "p5"}, |
|
{p1: "p14", p2: "p15"}, |
|
{p1: "p14", p2: "p1"}, |
|
{p1: "p14", p2: "p2"}, |
|
{p1: "p1", p2: "p16"}, |
|
{p1: "mong10", p2: "mong11", f: 0.5}, |
|
{p1: "mong11", p2: "mong12"}, |
|
{p1: "mong20", p2: "mong21", f: 0.5}, |
|
{p1: "mong21", p2: "mong22"}, |
|
{p1: "mong30", p2: "mong31", f: 0.1, len: canvas.height * 0.33}, |
|
{p1: "mong31", p2: "mong32"}, |
|
{p1: "mong11", p2: "mong21", f: 0.02}, |
|
{p1: "mong11", p2: "p14", f: 0.5}, |
|
{p1: "mong21", p2: "p14", f: 0.5} |
|
], |
|
angleConstraints: [ |
|
{p1: "p0", p2: "p3", p3: "p7", a: 1.2, r: Math.PI / 2.5, f: 0.2}, |
|
{p1: "p4", p2: "p5", p3: "p6", a: -1.2, r: Math.PI / 2.5, f: 0.2}, |
|
{p1: "p3", p2: "p7", p3: "p9", a: -1.2, r: Math.PI / 2, f: 0.2}, |
|
{p1: "p5", p2: "p6", p3: "p8", a: 1.2, r: Math.PI / 2, f: 0.2} |
|
], |
|
strokes: [ |
|
{c: "#fff", p1: "mong30", p2: "mong31"}, |
|
{c: "#fff", p1: "mong10", p2: "mong11"}, |
|
{c: "#fff", p1: "mong20", p2: "mong21"}, |
|
{c: "#fff", p1: "mong11", p2: "p14"}, |
|
{c: "#fff", p1: "mong21", p2: "p14"} |
|
], |
|
images: [ |
|
{img: "YinYan.png", p1: "mong31", p2: "mong32", ox: 250, oy: 250, w: 500, h: 500, a: Math.PI/2}, |
|
{img:"ballleft.png", p1: "mong11", p2: "mong12", ox: 139, oy: 139, w: 278, h: 278, a: -Math.PI/2}, |
|
{img:"ballright.png", p1: "mong21", p2: "mong22", ox: 139, oy: 139, w: 278, h: 278, a: 0}, |
|
{img:"foottoer.png", p1: "p6", p2: "p8", ox: 20, oy: 150, w: 447, h: 192, a: 0.15}, |
|
{img:"upperlegr.png", p1: "p5", p2: "p6", ox: 30, oy: 60, w: 330, h: 120, a: 0}, |
|
{img:"foottoe.png", p1: "p7", p2: "p9", ox: 20, oy: 50, w: 447, h: 192, a: -0.1}, |
|
{img:"upperleg.png", p1: "p3", p2: "p7", ox: 30, oy: 60, w: 330, h: 120, a: 0}, |
|
{img:"body.png", p1: "p2", p2: "p1", ox: 30, oy: 264 * 0.5, w: 514, h: 264, a: 0}, |
|
{img:"heady.png", p1: "p1", p2: "p16", ox: 272 / 2, oy: 272 / 2, w: 272, h: 271, a: 0}, |
|
{img:"heart.png", p1: "p14", p2: "p15", ox: 75, oy: 44, w: 88, h: 91, a: Math.PI}, |
|
{img:"upperarml.png", p1: "p0", p2: "p10", ox: 16, oy: 7, w: 287, h: 91, a: -0.12}, |
|
{img:"upperarm.png", p1: "p4", p2: "p11", ox: 16, oy: 83, w: 287, h: 91, a: 0.1}, |
|
{img:"handleft.png", p1: "p11", p2: "p13", ox: 30, oy: 44, w: 447, h: 95, a: 0}, |
|
{img:"handleft.png", p1: "p10", p2: "p12", ox: 30, oy: 44, w: 447, h:95, a: 0}, |
|
] |
|
}; |
|
////////////// sounds /////////////// |
|
let audio = null; |
|
if (window.Audio) { |
|
audio = [ |
|
new Audio(base + '1.mp3'), |
|
new Audio(base + '2.mp3'), |
|
new Audio(base + '3.mp3'), |
|
new Audio(base + '4.mp3'), |
|
new Audio(base + '5.mp3'), |
|
new Audio(base + 'YinYan.mp3') |
|
]; |
|
} |
|
/////////////// Build ////////////////// |
|
const init = struct => { |
|
for (let p in struct.points) { |
|
const o = struct.points[p]; |
|
const point = new Point(o); |
|
struct.points[p] = point; |
|
points.push(point); |
|
} |
|
for (const c of struct.constraints) { |
|
constraints.push( |
|
new Constraint(struct, c) |
|
); |
|
} |
|
for (const c of struct.angleConstraints) { |
|
constraints.push(new AngleConstraint(struct, c)); |
|
} |
|
for (const s of struct.strokes) { |
|
shapes.push(new Stroke(struct, s)); |
|
} |
|
for (const i of struct.images) { |
|
shapes.push(new Shape(struct, i)); |
|
} |
|
}; |
|
init(struct); |
|
run(); |
|
} |