Because it is fun, and why not?
Forked from Gerard Ferrandez's Pen yoga!.
A Pen by Anton Shtylman on CodePen.
<!-- | |
─────█─▄▀█──█▀▄─█───── | |
────▐▌──────────▐▌──── | |
────█▌▀▄──▄▄──▄▀▐█──── | |
───▐██──▀▀──▀▀──██▌─── | |
──▄████▄──▐▌──▄████▄── | |
--> |
;(function() { | |
'use strict'; | |
// variables | |
var DEBUG = false; | |
var points = null; | |
var constraints = null; | |
var skins = null; | |
var kGravity = 1.5; | |
var kFriction = 0.98; | |
var base = 'https://s3-us-west-2.amazonaws.com/s.cdpn.io/222599/'; | |
Math.sign = Math.sign || function(x) { | |
return x > 0 ? 1 : -1; | |
}; | |
// main loop | |
function run() { | |
requestAnimationFrame(run); | |
ctx.clearRect(0, 0, canvas.width, canvas.height); | |
// dragging | |
pointer.dragging(); | |
// static points | |
scene.static(); | |
// draw scene | |
ctx.save(); | |
var s = skins; | |
while (s) s = s.draw(); | |
ctx.restore(); | |
// verlet integration | |
var p = points; | |
while (p) p = p.integrate(); | |
// cursor | |
pointer.cursor(); | |
// solve constraints | |
var c = constraints; | |
while (c) c = c.solve(); | |
// show constraints and points | |
if (DEBUG) { | |
c = constraints; | |
while (c) c = c.draw(); | |
} | |
} | |
// point 2D constructor | |
function Point (x, y, radius, mass, gravity) { | |
this.x = canvas.width * 0.5 + x; | |
this.y = y; | |
this.oldX = this.x; | |
this.oldY = y; | |
this.radius = radius || 1; | |
this.mass = mass || 1.0; | |
this.friction = kFriction; | |
this.gravity = gravity || kGravity; | |
this.next = null; | |
points = iterator.link (points, this); | |
} | |
// set position | |
Point.prototype.position = function (x, y) { | |
this.x = x; | |
this.y = y; | |
}; | |
// verlet integration | |
Point.prototype.integrate = function () { | |
var x = this.x; | |
var y = this.y; | |
this.x += (this.x - this.oldX) * this.friction; | |
this.y += (this.y - this.oldY) * this.friction + this.gravity; | |
this.oldX = x; | |
this.oldY = y; | |
// bottom + friction | |
if (this.y > canvas.height - this.radius) { | |
this.x = x; | |
this.y = canvas.height - this.radius; | |
} | |
// cursor style | |
if (!pointer.draggable) { | |
var dx = this.x - pointer.x; | |
var dy = this.y - pointer.y; | |
if (Math.sqrt(dx * dx + dy * dy) < this.radius * 2) pointer.draggable = true; | |
} | |
return this.next; | |
}; | |
// calculate distance between 2 points | |
Point.prototype.dist = function (p) { | |
var dx = this.x - p.x; | |
var dy = this.y - p.y; | |
return Math.sqrt(dx * dx + dy * dy); | |
}; | |
// Angled Constraint constructor | |
function AngleConstraint (p1, p2, p3, angle, range, force) { | |
this.p1 = p1; | |
this.p2 = p2; | |
this.p3 = p3; | |
this.len1 = p1.dist(p2); | |
this.len2 = p2.dist(p3); | |
this.angle = angle; | |
this.range = range; | |
this.force = force || 0.2; | |
constraints = iterator.link (constraints, this); | |
} | |
// solve 2 vectors angled (+ stick) constraint | |
// http://stackoverflow.com/questions/16336702/ragdoll-joint-angle-constraints | |
AngleConstraint.prototype.solve = function () { | |
var a, b, c, e, m, m1, m2, m3, x1, y1, cos, sin; | |
a = Math.atan2(this.p2.y - this.p1.y, this.p2.x - this.p1.x); | |
b = Math.atan2(this.p3.y - this.p2.y, this.p3.x - this.p2.x); | |
c = this.angle - (b - a); | |
c = c > Math.PI ? (c - 2 * Math.PI) : (c < -Math.PI ? (c + 2 * Math.PI) : c); | |
e = (Math.abs(c) > this.range) ? (-Math.sign(c) * this.range + c) * this.force : 0; | |
m = this.p1.mass + this.p2.mass; | |
m1 = this.p1.mass / m; | |
m2 = this.p2.mass / m; | |
cos = Math.cos(a - e); | |
sin = Math.sin(a - e); | |
x1 = this.p1.x + (this.p2.x - this.p1.x) * m2; | |
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; | |
a = Math.atan2(this.p2.y - this.p3.y, this.p2.x - this.p3.x) + e; | |
m = this.p2.mass + this.p3.mass; | |
m2 = this.p2.mass / m; | |
m3 = this.p3.mass / m; | |
cos = Math.cos(a); | |
sin = Math.sin(a); | |
x1 = this.p3.x + (this.p2.x - this.p3.x) * m2; | |
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; | |
return this.next; | |
}; | |
// draw angle constraint (DEBUG mode) | |
AngleConstraint.prototype.draw = function () { | |
ctx.beginPath(); | |
ctx.moveTo (this.p1.x, this.p1.y); | |
ctx.lineTo(this.p2.x, this.p2.y); | |
ctx.lineTo(this.p3.x, this.p3.y); | |
ctx.stroke(); | |
ctx.beginPath(); | |
ctx.arc(this.p1.x, this.p1.y, this.p1.radius * 2, 0, Math.PI * 2); | |
ctx.stroke(); | |
ctx.beginPath(); | |
ctx.arc(this.p2.x, this.p2.y, this.p2.radius * 2, 0, Math.PI * 2); | |
ctx.stroke(); | |
ctx.beginPath(); | |
ctx.arc(this.p3.x, this.p3.y, this.p3.radius * 2, 0, Math.PI * 2); | |
ctx.stroke(); | |
return this.next; | |
}; | |
// simple stick constraint constructor | |
function Constraint (p1, p2, force, len) { | |
this.p1 = p1; | |
this.p2 = p2; | |
this.len = len || p1.dist(p2); | |
this.force = force || 2; | |
constraints = iterator.link (constraints, this); | |
} | |
// solve stick constraint | |
Constraint.prototype.solve = function () { | |
var d, dx, dy, s1, s2, tm; | |
dx = this.p1.x - this.p2.x; | |
dy = this.p1.y - this.p2.y; | |
d = Math.sqrt(dx * dx + dy * dy); | |
tm = this.p1.mass + this.p2.mass; | |
d = (d - (d + (this.len - d) * this.force)) / d * 0.5; | |
s1 = d * (this.p1.mass / tm); | |
s2 = d * (this.p2.mass / tm); | |
this.p1.x = this.p1.x - dx * s2; | |
this.p1.y = this.p1.y - dy * s2; | |
this.p2.x = this.p2.x + dx * s1; | |
this.p2.y = this.p2.y + dy * s1; | |
return this.next; | |
}; | |
// draw constraint (DEBUG mode) | |
Constraint.prototype.draw = function () { | |
ctx.beginPath(); | |
ctx.moveTo (this.p1.x, this.p1.y); | |
ctx.lineTo(this.p2.x, this.p2.y); | |
ctx.stroke(); | |
ctx.beginPath(); | |
ctx.arc(this.p1.x, this.p1.y, this.p1.radius * 2, 0, Math.PI * 2); | |
ctx.stroke(); | |
ctx.beginPath(); | |
ctx.arc(this.p2.x, this.p2.y, this.p2.radius * 2, 0, Math.PI * 2); | |
ctx.stroke(); | |
return this.next; | |
}; | |
// skin constructor | |
function Skin (img, p1, p2, offsetX, offsetY, width, height, angle) { | |
this.p1 = p1; | |
this.p2 = p2; | |
this.img = new Image(); | |
this.img.src = base + img; | |
this.ox = offsetX; | |
this.oy = offsetY; | |
this.w = width; | |
this.h = height; | |
this.angle = angle || 0; | |
this.ctx = ctx; | |
skins = iterator.link(skins, this); | |
} | |
// draw skin | |
Skin.prototype.draw = function () { | |
var a = Math.atan2((this.p2.y - this.p1.y), (this.p2.x - this.p1.x)); | |
var cos = Math.cos(a + this.angle); | |
var sin = Math.sin(a + this.angle); | |
this.ctx.setTransform(cos, sin, -sin, cos, this.p1.x, this.p1.y); | |
this.ctx.drawImage(this.img, -this.ox, -this.oy, this.w, this.h); | |
return this.next; | |
}; | |
// Stroke constructor | |
function Stroke (color, p1, p2) { | |
this.p1 = p1; | |
this.p2 = p2; | |
skins = iterator.link(skins, this); | |
} | |
// draw stroke | |
Stroke.prototype.draw = function () { | |
ctx.beginPath(); | |
ctx.strokeStyle = this.color; | |
ctx.moveTo(this.p1.x, this.p1.y); | |
ctx.lineTo(this.p2.x, this.p2.y); | |
ctx.stroke(); | |
return this.next; | |
}; | |
// set canvas | |
var canvas = { | |
width: 0, | |
height: 0, | |
elem: document.createElement('canvas'), | |
resize: function () { | |
this.width = this.elem.width = this.elem.offsetWidth; | |
this.height = this.elem.height = this.elem.offsetHeight; | |
}, | |
init: function () { | |
var ctx = canvas.elem.getContext('2d'); | |
document.body.appendChild(canvas.elem); | |
window.addEventListener('resize', canvas.resize.bind(canvas), false); | |
return ctx; | |
} | |
}; | |
var ctx = canvas.init(); | |
// iterator | |
var iterator = { | |
last: null, | |
link: function (list, object) { | |
if (!list) list = object; | |
if (this.last) this.last.next = object; | |
this.last = object; | |
return list; | |
} | |
}; | |
// set pointer / touch | |
var 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) { | |
var pointer; | |
if (e.targetTouches) { | |
e.preventDefault(); | |
pointer = e.targetTouches[0] | |
} else pointer = e; | |
return pointer; | |
}, | |
addEvents: function () { | |
[ | |
[window, 'mousemove,touchmove', function (e) { | |
var pointer = this.pointer(e); | |
this.x = pointer.clientX; | |
this.y = pointer.clientY; | |
}], | |
[canvas.elem, 'mousedown,touchstart', function (e) { | |
if (!this.drag) { | |
var pointer = this.pointer(e); | |
this.x = pointer.clientX; | |
this.y = pointer.clientY; | |
var dm = 9999, p = points; | |
while (p) { | |
var dx = p.x - this.x; | |
var dy = p.y - this.y; | |
var d = Math.sqrt(dx * dx + dy * dy); | |
if (d < p.radius * 2) { | |
if (d < dm) { | |
dm = d; | |
this.drag = p; | |
} | |
} | |
p = p.next; | |
} | |
} | |
}], | |
[window, 'mouseup,touchend,touchcancel', function () { | |
this.drag = null; | |
}] | |
].forEach(function (e) { | |
for (var i = 0, events = e[1].split(','); i < events.length; i++) { | |
e[0].addEventListener(events[i], e[2].bind(this), false ); | |
} | |
}.bind(this)); | |
} | |
}; | |
// add events | |
pointer.addEvents(); | |
// resize | |
canvas.resize(); | |
// scene definition | |
var scene = { | |
points: { | |
p0: [-70, -80, 20, 1], | |
p1: [0 , -200, 40, 1], | |
p2: [ 0, 100, 20, 1], | |
p3: [-46, 100, 20, 1], | |
p4: [ 75, -75, 20, 1], | |
p5: [ 50, 104, 20, 1], | |
p6: [ 50, 244, 20, 0.5], | |
p7: [ -46, 244, 20, 0.5], | |
p8: [ 50, 450, 15, 0.35], | |
p9: [-46, 450, 15, 0.35], | |
p10: [ -181, -80, 20, 0.5], | |
p11: [ 187, -75, 20, 0.5], | |
p12: [ -340, -80, 12, 0.35, -0.02], | |
p13: [ 345, -75, 12, 0.35, -0.02], | |
p14: [ 0, -80], | |
p15: [ 0, -40], | |
cloudLeft1: [ 0, 0, 0, 0], | |
cloudLeft2: [ 0, 0, 0, 0], | |
cloudRight1: [ 0, 0, 0, 0], | |
cloudRight2: [ 0, 0, 0, 0], | |
mong10: [0, -50 - canvas.height / 6], | |
mong11: [0, 20, 30], | |
mong12: [0, 160, 30], | |
mong20: [0, -50 - canvas.height / 6], | |
mong21: [0, 20, 30], | |
mong22: [0, 160, 30], | |
mong30: [0, -150, 0, 1, 0], | |
mong31: [0, canvas.height / 2 - 150, 0, 1, -0.25], | |
mong32: [0, canvas.height / 2, 80, 1, -0.25] | |
}, | |
static: function () { | |
this.points.cloudLeft1.position(0, 0); | |
this.points.cloudLeft2.position(100, 0); | |
this.points.cloudRight1.position(canvas.width - 183, 0); | |
this.points.cloudRight2.position(canvas.width, 0); | |
this.points.mong10.position(300, -100); | |
this.points.mong20.position(canvas.width - 300, -100); | |
this.points.mong30.position(300, canvas.height + 100); | |
}, | |
constraints: [ | |
['p0','p1'], | |
['p1','p2'], | |
['p2','p3'], | |
['p0','p2'], | |
['p1','p3'], | |
['p1','p4'], | |
['p5','p2'], | |
['p1','p5'], | |
['p2','p4'], | |
['p0','p4'], | |
['p3','p5'], | |
['p3','p4'], | |
['p0','p10'], | |
['p4','p11'], | |
['p10','p12'], | |
['p11','p13'], | |
['p14','p3'], | |
['p14','p0'], | |
['p14','p4'], | |
['p14','p5'], | |
['p14','p15'], | |
['p14','p1'], | |
['p14','p2'], | |
['mong10','mong11'], | |
['mong11','mong12'], | |
['mong20','mong21'], | |
['mong21','mong22'], | |
['mong30','mong31'], | |
['mong31','mong32'], | |
['mong12','p14', 0, canvas.width / 6], | |
['mong22','p14', 0, canvas.width / 6] | |
], | |
angleConstraints: [ | |
['p0', 'p3', 'p7', 1.2, Math.PI / 1.8, 0.2], | |
['p4', 'p5', 'p6', -1.2, Math.PI / 1.8, 0.2], | |
['p3', 'p7', 'p9', -1.2, Math.PI / 1.8, 0.2], | |
['p5', 'p6', 'p8', 1.2, Math.PI / 1.8, 0.2] | |
], | |
strokes: [ | |
['#000', 'mong30', 'mong31'], | |
['#000', 'mong10', 'mong11'], | |
['#000', 'mong20', 'mong21'], | |
['#000', 'mong12', 'p14'], | |
['#000', 'mong22', 'p14'] | |
], | |
// pics credits Lindsey Carr - https://www.pinterest.com/catheyhaynie/lindsey-carr/ | |
images: [ | |
['gd14.png', 'cloudLeft1', 'cloudLeft2', 0, 0, 195, 188], | |
['gd15.png', 'cloudRight1', 'cloudRight2', 0, 0, 183, 188], | |
['gd16.png', 'mong31', 'mong32', 305, 305 / 2, 325, 305, Math.PI], | |
['gd20.png', 'mong11', 'mong12', 0, 102 / 2, 150, 102], | |
['gd19.png', 'mong21', 'mong22', 0, 102 / 2, 144, 102], | |
['gd6.png', 'p6', 'p8', 20, 50, 246, 84, 0.16], | |
['gd12.png', 'p5', 'p6', 24, 35, 188, 71], | |
['gd13.png', 'p7', 'p9', 20, 35, 246, 84, -0.16], | |
['gd5.png', 'p3', 'p7', 24, 35, 189, 69], | |
['gd1.png', 'p2', 'p1', 30, 181 / 2, 375, 181], | |
['gd18.png', 'p14', 'p15', 5, 140 / 6, 157 / 2, 140 / 3], | |
['gd2.png', 'p0', 'p10', 22, 26, 148, 49], | |
['gd2.png', 'p4', 'p11', 22, 26, 148, 49], | |
['gd3.png', 'p11', 'p13', 17, 22, 199, 43], | |
['gd11.png', 'p10', 'p12', 17, 22, 199, 43] | |
], | |
build: function () { | |
var p, i, c, o, s; | |
iterator.last = null; | |
for (p in scene.points) { | |
o = scene.points[p]; | |
scene.points[p] = new Point (o[0], o[1], o[2], o[3], o[4]); | |
} | |
// build stick constraints | |
iterator.last = null; | |
for (i = 0; i < scene.constraints.length; i++) { | |
c = scene.constraints[i]; | |
new Constraint (scene.points[c[0]], scene.points[c[1]], c[2], c[3]); | |
} | |
// build angle constraints | |
for (i = 0; i < scene.angleConstraints.length; i++) { | |
c = scene.angleConstraints[i]; | |
new AngleConstraint (scene.points[c[0]], scene.points[c[1]], scene.points[c[2]], c[3], c[4], c[5]); | |
} | |
// build strokes | |
iterator.last = null; | |
for (i = 0; i < scene.strokes.length; i++) { | |
s = scene.strokes[i]; | |
new Stroke (s[0], scene.points[s[1]], scene.points[s[2]]); | |
} | |
// build images | |
for (i = 0; i < scene.images.length; i++) { | |
s = scene.images[i]; | |
new Skin (s[0], scene.points[s[1]], scene.points[s[2]], s[3], s[4], s[5], s[6], s[7]); | |
} | |
} | |
}; | |
// build scene | |
scene.build(); | |
// start animation | |
run(); | |
}()); |
html { | |
overflow: hidden; | |
touch-action: none; | |
content-zooming: none; | |
} | |
body { | |
position: absolute; | |
margin: 0; | |
padding: 0; | |
width: 100%; | |
height: 100%; | |
background: #000; | |
} | |
canvas { | |
position:absolute; | |
width:100%; | |
height:100%; | |
background: #c9c4b5; | |
} | |
.draggable { | |
cursor: pointer; | |
cursor: -webkit-grab; | |
} | |
.dragging { | |
cursor: move; | |
cursor: -webkit-grabbing; | |
} |
Because it is fun, and why not?
Forked from Gerard Ferrandez's Pen yoga!.
A Pen by Anton Shtylman on CodePen.