I wrote my first algorithm for procedural animation using inverse kinematics. Feel free to tinker. If you want to use it in a project of your own, just give me some credit and feel free.
A Pen by MacSearlas on CodePen.
I wrote my first algorithm for procedural animation using inverse kinematics. Feel free to tinker. If you want to use it in a project of your own, just give me some credit and feel free.
A Pen by MacSearlas on CodePen.
var Input = { | |
keys: [], | |
mouse: { | |
left: false, | |
right: false, | |
middle: false, | |
x: 0, | |
y: 0 | |
} | |
}; | |
for (var i = 0; i < 230; i++) { | |
Input.keys.push(false); | |
} | |
document.addEventListener("keydown", function(event) { | |
Input.keys[event.keyCode] = true; | |
}); | |
document.addEventListener("keyup", function(event) { | |
Input.keys[event.keyCode] = false; | |
}); | |
document.addEventListener("mousedown", function(event) { | |
if ((event.button = 0)) { | |
Input.mouse.left = true; | |
} | |
if ((event.button = 1)) { | |
Input.mouse.middle = true; | |
} | |
if ((event.button = 2)) { | |
Input.mouse.right = true; | |
} | |
}); | |
document.addEventListener("mouseup", function(event) { | |
if ((event.button = 0)) { | |
Input.mouse.left = false; | |
} | |
if ((event.button = 1)) { | |
Input.mouse.middle = false; | |
} | |
if ((event.button = 2)) { | |
Input.mouse.right = false; | |
} | |
}); | |
document.addEventListener("mousemove", function(event) { | |
Input.mouse.x = event.clientX; | |
Input.mouse.y = event.clientY; | |
}); | |
//Sets up canvas | |
var canvas = document.createElement("canvas"); | |
document.body.appendChild(canvas); | |
canvas.width = Math.max(window.innerWidth, window.innerWidth); | |
canvas.height = Math.max(window.innerWidth, window.innerWidth); | |
canvas.style.position = "absolute"; | |
canvas.style.left = "0px"; | |
canvas.style.top = "0px"; | |
document.body.style.overflow = "hidden"; | |
var ctx = canvas.getContext("2d"); | |
//Necessary classes | |
var segmentCount = 0; | |
class Segment { | |
constructor(parent, size, angle, range, stiffness) { | |
segmentCount++; | |
this.isSegment = true; | |
this.parent = parent; //Segment which this one is connected to | |
if (typeof parent.children == "object") { | |
parent.children.push(this); | |
} | |
this.children = []; //Segments connected to this segment | |
this.size = size; //Distance from parent | |
this.relAngle = angle; //Angle relative to parent | |
this.defAngle = angle; //Default angle relative to parent | |
this.absAngle = parent.absAngle + angle; //Angle relative to x-axis | |
this.range = range; //Difference between maximum and minimum angles | |
this.stiffness = stiffness; //How closely it conforms to default angle | |
this.updateRelative(false, true); | |
} | |
updateRelative(iter, flex) { | |
this.relAngle = | |
this.relAngle - | |
2 * | |
Math.PI * | |
Math.floor((this.relAngle - this.defAngle) / 2 / Math.PI + 1 / 2); | |
if (flex) { | |
// this.relAngle=this.range/ | |
// (1+Math.exp(-4*(this.relAngle-this.defAngle)/ | |
// (this.stiffness*this.range))) | |
// -this.range/2+this.defAngle; | |
this.relAngle = Math.min( | |
this.defAngle + this.range / 2, | |
Math.max( | |
this.defAngle - this.range / 2, | |
(this.relAngle - this.defAngle) / this.stiffness + this.defAngle | |
) | |
); | |
} | |
this.absAngle = this.parent.absAngle + this.relAngle; | |
this.x = this.parent.x + Math.cos(this.absAngle) * this.size; //Position | |
this.y = this.parent.y + Math.sin(this.absAngle) * this.size; //Position | |
if (iter) { | |
for (var i = 0; i < this.children.length; i++) { | |
this.children[i].updateRelative(iter, flex); | |
} | |
} | |
} | |
draw(iter) { | |
ctx.beginPath(); | |
ctx.moveTo(this.parent.x, this.parent.y); | |
ctx.lineTo(this.x, this.y); | |
ctx.stroke(); | |
if (iter) { | |
for (var i = 0; i < this.children.length; i++) { | |
this.children[i].draw(true); | |
} | |
} | |
} | |
follow(iter) { | |
var x = this.parent.x; | |
var y = this.parent.y; | |
var dist = ((this.x - x) ** 2 + (this.y - y) ** 2) ** 0.5; | |
this.x = x + this.size * (this.x - x) / dist; | |
this.y = y + this.size * (this.y - y) / dist; | |
this.absAngle = Math.atan2(this.y - y, this.x - x); | |
this.relAngle = this.absAngle - this.parent.absAngle; | |
this.updateRelative(false, true); | |
//this.draw(); | |
if (iter) { | |
for (var i = 0; i < this.children.length; i++) { | |
this.children[i].follow(true); | |
} | |
} | |
} | |
} | |
class LimbSystem { | |
constructor(end, length, speed, creature) { | |
this.end = end; | |
this.length = Math.max(1, length); | |
this.creature = creature; | |
this.speed = speed; | |
creature.systems.push(this); | |
this.nodes = []; | |
var node = end; | |
for (var i = 0; i < length; i++) { | |
this.nodes.unshift(node); | |
//node.stiffness=1; | |
node = node.parent; | |
if (!node.isSegment) { | |
this.length = i + 1; | |
break; | |
} | |
} | |
this.hip = this.nodes[0].parent; | |
} | |
moveTo(x, y) { | |
this.nodes[0].updateRelative(true, true); | |
var dist = ((x - this.end.x) ** 2 + (y - this.end.y) ** 2) ** 0.5; | |
var len = Math.max(0, dist - this.speed); | |
for (var i = this.nodes.length - 1; i >= 0; i--) { | |
var node = this.nodes[i]; | |
var ang = Math.atan2(node.y - y, node.x - x); | |
node.x = x + len * Math.cos(ang); | |
node.y = y + len * Math.sin(ang); | |
x = node.x; | |
y = node.y; | |
len = node.size; | |
} | |
for (var i = 0; i < this.nodes.length; i++) { | |
var node = this.nodes[i]; | |
node.absAngle = Math.atan2( | |
node.y - node.parent.y, | |
node.x - node.parent.x | |
); | |
node.relAngle = node.absAngle - node.parent.absAngle; | |
for (var ii = 0; ii < node.children.length; ii++) { | |
var childNode = node.children[ii]; | |
if (!this.nodes.includes(childNode)) { | |
childNode.updateRelative(true, false); | |
} | |
} | |
} | |
//this.nodes[0].updateRelative(true,false) | |
} | |
update() { | |
this.moveTo(Input.mouse.x, Input.mouse.y); | |
} | |
} | |
class LegSystem extends LimbSystem { | |
constructor(end, length, speed, creature) { | |
super(end, length, speed, creature); | |
this.goalX = end.x; | |
this.goalY = end.y; | |
this.step = 0; //0 stand still, 1 move forward,2 move towards foothold | |
this.forwardness = 0; | |
//For foot goal placement | |
this.reach = | |
0.9 * | |
((this.end.x - this.hip.x) ** 2 + (this.end.y - this.hip.y) ** 2) ** 0.5; | |
var relAngle = | |
this.creature.absAngle - | |
Math.atan2(this.end.y - this.hip.y, this.end.x - this.hip.x); | |
relAngle -= 2 * Math.PI * Math.floor(relAngle / 2 / Math.PI + 1 / 2); | |
this.swing = -relAngle + (2 * (relAngle < 0) - 1) * Math.PI / 2; | |
this.swingOffset = this.creature.absAngle - this.hip.absAngle; | |
//this.swing*=(2*(relAngle>0)-1); | |
} | |
update(x, y) { | |
this.moveTo(this.goalX, this.goalY); | |
//this.nodes[0].follow(true,true) | |
if (this.step == 0) { | |
var dist = | |
((this.end.x - this.goalX) ** 2 + (this.end.y - this.goalY) ** 2) ** | |
0.5; | |
if (dist > 1) { | |
this.step = 1; | |
//this.goalX=x; | |
//this.goalY=y; | |
this.goalX = | |
this.hip.x + | |
this.reach * | |
Math.cos(this.swing + this.hip.absAngle + this.swingOffset) + | |
(2 * Math.random() - 1) * this.reach / 2; | |
this.goalY = | |
this.hip.y + | |
this.reach * | |
Math.sin(this.swing + this.hip.absAngle + this.swingOffset) + | |
(2 * Math.random() - 1) * this.reach / 2; | |
} | |
} else if (this.step == 1) { | |
var theta = | |
Math.atan2(this.end.y - this.hip.y, this.end.x - this.hip.x) - | |
this.hip.absAngle; | |
var dist = | |
((this.end.x - this.hip.x) ** 2 + (this.end.y - this.hip.y) ** 2) ** | |
0.5; | |
var forwardness2 = dist * Math.cos(theta); | |
var dF = this.forwardness - forwardness2; | |
this.forwardness = forwardness2; | |
if (dF * dF < 1) { | |
this.step = 0; | |
this.goalX = this.hip.x + (this.end.x - this.hip.x); | |
this.goalY = this.hip.y + (this.end.y - this.hip.y); | |
} | |
} | |
// ctx.strokeStyle='blue'; | |
// ctx.beginPath(); | |
// ctx.moveTo(this.end.x,this.end.y); | |
// ctx.lineTo(this.hip.x+this.reach*Math.cos(this.swing+this.hip.absAngle+this.swingOffset), | |
// this.hip.y+this.reach*Math.sin(this.swing+this.hip.absAngle+this.swingOffset)); | |
// ctx.stroke(); | |
// ctx.strokeStyle='black'; | |
} | |
} | |
class Creature { | |
constructor( | |
x, | |
y, | |
angle, | |
fAccel, | |
fFric, | |
fRes, | |
fThresh, | |
rAccel, | |
rFric, | |
rRes, | |
rThresh | |
) { | |
this.x = x; //Starting position | |
this.y = y; | |
this.absAngle = angle; //Staring angle | |
this.fSpeed = 0; //Forward speed | |
this.fAccel = fAccel; //Force when moving forward | |
this.fFric = fFric; //Friction against forward motion | |
this.fRes = fRes; //Resistance to motion | |
this.fThresh = fThresh; //minimum distance to target to keep moving forward | |
this.rSpeed = 0; //Rotational speed | |
this.rAccel = rAccel; //Force when rotating | |
this.rFric = rFric; //Friction against rotation | |
this.rRes = rRes; //Resistance to rotation | |
this.rThresh = rThresh; //Maximum angle difference before rotation | |
this.children = []; | |
this.systems = []; | |
} | |
follow(x, y) { | |
var dist = ((this.x - x) ** 2 + (this.y - y) ** 2) ** 0.5; | |
var angle = Math.atan2(y - this.y, x - this.x); | |
//Update forward | |
var accel = this.fAccel; | |
if (this.systems.length > 0) { | |
var sum = 0; | |
for (var i = 0; i < this.systems.length; i++) { | |
sum += this.systems[i].step == 0; | |
} | |
accel *= sum / this.systems.length; | |
} | |
this.fSpeed += accel * (dist > this.fThresh); | |
this.fSpeed *= 1 - this.fRes; | |
this.speed = Math.max(0, this.fSpeed - this.fFric); | |
//Update rotation | |
var dif = this.absAngle - angle; | |
dif -= 2 * Math.PI * Math.floor(dif / (2 * Math.PI) + 1 / 2); | |
if (Math.abs(dif) > this.rThresh && dist > this.fThresh) { | |
this.rSpeed -= this.rAccel * (2 * (dif > 0) - 1); | |
} | |
this.rSpeed *= 1 - this.rRes; | |
if (Math.abs(this.rSpeed) > this.rFric) { | |
this.rSpeed -= this.rFric * (2 * (this.rSpeed > 0) - 1); | |
} else { | |
this.rSpeed = 0; | |
} | |
//Update position | |
this.absAngle += this.rSpeed; | |
this.absAngle -= | |
2 * Math.PI * Math.floor(this.absAngle / (2 * Math.PI) + 1 / 2); | |
this.x += this.speed * Math.cos(this.absAngle); | |
this.y += this.speed * Math.sin(this.absAngle); | |
this.absAngle += Math.PI; | |
for (var i = 0; i < this.children.length; i++) { | |
this.children[i].follow(true, true); | |
} | |
for (var i = 0; i < this.systems.length; i++) { | |
this.systems[i].update(x, y); | |
} | |
this.absAngle -= Math.PI; | |
this.draw(true); | |
} | |
draw(iter) { | |
var r = 4; | |
ctx.beginPath(); | |
ctx.arc( | |
this.x, | |
this.y, | |
r, | |
Math.PI / 4 + this.absAngle, | |
7 * Math.PI / 4 + this.absAngle | |
); | |
ctx.moveTo( | |
this.x + r * Math.cos(7 * Math.PI / 4 + this.absAngle), | |
this.y + r * Math.sin(7 * Math.PI / 4 + this.absAngle) | |
); | |
ctx.lineTo( | |
this.x + r * Math.cos(this.absAngle) * 2 ** 0.5, | |
this.y + r * Math.sin(this.absAngle) * 2 ** 0.5 | |
); | |
ctx.lineTo( | |
this.x + r * Math.cos(Math.PI / 4 + this.absAngle), | |
this.y + r * Math.sin(Math.PI / 4 + this.absAngle) | |
); | |
ctx.stroke(); | |
if (iter) { | |
for (var i = 0; i < this.children.length; i++) { | |
this.children[i].draw(true); | |
} | |
} | |
} | |
} | |
//Initializes and animates | |
var critter; | |
function setupSimple() { | |
//(x,y,angle,fAccel,fFric,fRes,fThresh,rAccel,rFric,rRes,rThresh) | |
var critter = new Creature( | |
window.innerWidth / 2, | |
window.innerHeight / 2, | |
0, | |
12, | |
1, | |
0.5, | |
16, | |
0.5, | |
0.085, | |
0.5, | |
0.3 | |
); | |
var node = critter; | |
//(parent,size,angle,range,stiffness) | |
for (var i = 0; i < 128; i++) { | |
var node = new Segment(node, 8, 0, 3.14159 / 2, 1); | |
} | |
setInterval(function() { | |
ctx.clearRect(0, 0, canvas.width, canvas.height); | |
critter.follow(Input.mouse.x, Input.mouse.y); | |
}, 33); | |
} | |
function setupTentacle() { | |
//(x,y,angle,fAccel,fFric,fRes,fThresh,rAccel,rFric,rRes,rThresh) | |
critter = new Creature( | |
window.innerWidth / 2, | |
window.innerHeight / 2, | |
0, | |
12, | |
1, | |
0.5, | |
16, | |
0.5, | |
0.085, | |
0.5, | |
0.3 | |
); | |
var node = critter; | |
//(parent,size,angle,range,stiffness) | |
for (var i = 0; i < 32; i++) { | |
var node = new Segment(node, 8, 0, 2, 1); | |
} | |
//(end,length,speed,creature) | |
var tentacle = new LimbSystem(node, 32, 8, critter); | |
setInterval(function() { | |
ctx.clearRect(0, 0, canvas.width, canvas.height); | |
critter.follow(canvas.width / 2, canvas.height / 2); | |
ctx.beginPath(); | |
ctx.arc(Input.mouse.x, Input.mouse.y, 2, 0, 6.283); | |
ctx.fill(); | |
}, 33); | |
} | |
function setupArm() { | |
//(x,y,angle,fAccel,fFric,fRes,fThresh,rAccel,rFric,rRes,rThresh) | |
var critter = new Creature( | |
window.innerWidth / 2, | |
window.innerHeight / 2, | |
0, | |
12, | |
1, | |
0.5, | |
16, | |
0.5, | |
0.085, | |
0.5, | |
0.3 | |
); | |
var node = critter; | |
//(parent,size,angle,range,stiffness) | |
for (var i = 0; i < 3; i++) { | |
var node = new Segment(node, 80, 0, 3.1416, 1); | |
} | |
var tentacle = new LimbSystem(node, 3, critter); | |
setInterval(function() { | |
ctx.clearRect(0, 0, canvas.width, canvas.height); | |
critter.follow(canvas.width / 2, canvas.height / 2); | |
}, 33); | |
ctx.beginPath(); | |
ctx.arc(Input.mouse.x, Input.mouse.y, 2, 0, 6.283); | |
ctx.fill(); | |
} | |
function setupTestSquid(size, legs) { | |
//(x,y,angle,fAccel,fFric,fRes,fThresh,rAccel,rFric,rRes,rThresh) | |
critter = new Creature( | |
window.innerWidth / 2, | |
window.innerHeight / 2, | |
0, | |
size * 10, | |
size * 3, | |
0.5, | |
16, | |
0.5, | |
0.085, | |
0.5, | |
0.3 | |
); | |
var legNum = legs; | |
var jointNum = 32; | |
for (var i = 0; i < legNum; i++) { | |
var node = critter; | |
var ang = Math.PI / 2 * (i / (legNum - 1) - 0.5); | |
for (var ii = 0; ii < jointNum; ii++) { | |
var node = new Segment( | |
node, | |
size * 64 / jointNum, | |
ang * (ii == 0), | |
3.1416, | |
1.2 | |
); | |
} | |
//(end,length,speed,creature,dist) | |
var leg = new LegSystem(node, jointNum, size * 30, critter); | |
} | |
setInterval(function() { | |
ctx.clearRect(0, 0, canvas.width, canvas.height); | |
critter.follow(Input.mouse.x, Input.mouse.y); | |
}, 33); | |
} | |
function setupLizard(size, legs, tail) { | |
var s = size; | |
//(x,y,angle,fAccel,fFric,fRes,fThresh,rAccel,rFric,rRes,rThresh) | |
critter = new Creature( | |
window.innerWidth / 2, | |
window.innerHeight / 2, | |
0, | |
s * 10, | |
s * 2, | |
0.5, | |
16, | |
0.5, | |
0.085, | |
0.5, | |
0.3 | |
); | |
var spinal = critter; | |
//(parent,size,angle,range,stiffness) | |
//Neck | |
for (var i = 0; i < 6; i++) { | |
spinal = new Segment(spinal, s * 4, 0, 3.1415 * 2 / 3, 1.1); | |
for (var ii = -1; ii <= 1; ii += 2) { | |
var node = new Segment(spinal, s * 3, ii, 0.1, 2); | |
for (var iii = 0; iii < 3; iii++) { | |
node = new Segment(node, s * 0.1, -ii * 0.1, 0.1, 2); | |
} | |
} | |
} | |
//Torso and legs | |
for (var i = 0; i < legs; i++) { | |
if (i > 0) { | |
//Vertebrae and ribs | |
for (var ii = 0; ii < 6; ii++) { | |
spinal = new Segment(spinal, s * 4, 0, 1.571, 1.5); | |
for (var iii = -1; iii <= 1; iii += 2) { | |
var node = new Segment(spinal, s * 3, iii * 1.571, 0.1, 1.5); | |
for (var iv = 0; iv < 3; iv++) { | |
node = new Segment(node, s * 3, -iii * 0.3, 0.1, 2); | |
} | |
} | |
} | |
} | |
//Legs and shoulders | |
for (var ii = -1; ii <= 1; ii += 2) { | |
var node = new Segment(spinal, s * 12, ii * 0.785, 0, 8); //Hip | |
node = new Segment(node, s * 16, -ii * 0.785, 6.28, 1); //Humerus | |
node = new Segment(node, s * 16, ii * 1.571, 3.1415, 2); //Forearm | |
for ( | |
var iii = 0; | |
iii < 4; | |
iii++ //fingers | |
) { | |
new Segment(node, s * 4, (iii / 3 - 0.5) * 1.571, 0.1, 4); | |
} | |
new LegSystem(node, 3, s * 12, critter, 4); | |
} | |
} | |
//Tail | |
for (var i = 0; i < tail; i++) { | |
spinal = new Segment(spinal, s * 4, 0, 3.1415 * 2 / 3, 1.1); | |
for (var ii = -1; ii <= 1; ii += 2) { | |
var node = new Segment(spinal, s * 3, ii, 0.1, 2); | |
for (var iii = 0; iii < 3; iii++) { | |
node = new Segment(node, s * 3 * (tail - i) / tail, -ii * 0.1, 0.1, 2); | |
} | |
} | |
} | |
setInterval(function() { | |
ctx.clearRect(0, 0, canvas.width, canvas.height); | |
critter.follow(Input.mouse.x, Input.mouse.y); | |
}, 33); | |
} | |
canvas.style.backgroundColor = "black"; | |
ctx.strokeStyle = "white"; | |
//setupSimple();//Just the very basic string | |
//setupTentacle();//Tentacle that reaches for mouse | |
//setupLizard(.5,100,128);//Literal centipede | |
//setupSquid(2,8);//Spidery thing | |
var legNum = Math.floor(1 + Math.random() * 12); | |
setupLizard( | |
8 / Math.sqrt(legNum), | |
legNum, | |
Math.floor(4 + Math.random() * legNum * 8) | |
); |