<canvas width="600" height="450"></canvas>

<p>Speed <input class="speed-range" type="range" min="0" max="0.2" step="0.01" value="0.07"></p>
body { text-align: center; }
canvas { display: block; margin: 0 auto; }
/*jshint unused: false, undef: true */
/*global blockSize: false */

// ----- utils ----- //

// extends objects
function extend( a, b ) {
  for ( var prop in b ) {
    a[ prop ] = b[ prop ];
  }
  return a;
}

function modulo( num, div ) {
  return ( ( num % div ) + div ) % div;
}

function normalizeAngle( angle ) {
  return modulo( angle, Math.PI * 2 );
}

function getDegrees( angle ) {
  return angle * ( 180 / Math.PI );
}



// --------------------------  -------------------------- //

function line( ctx, a, b ) {
  ctx.beginPath();
  ctx.moveTo( a.x, a.y );
  ctx.lineTo( b.x, b.y );
  ctx.stroke();
  ctx.closePath();
}

/*jshint browser: true, undef: true, unused: true */

// -------------------------- vector -------------------------- //

function Vector( x, y ) {
  this.x = x || 0;
  this.y = y || 0;
}

Vector.prototype.set = function( v ) {
  this.x = v.x;
  this.y = v.y;
};

Vector.prototype.setCoords = function( x, y ) {
  this.x = x;
  this.y = y;
}

Vector.prototype.add = function( v ) {
  this.x += v.x;
  this.y += v.y;
};

Vector.prototype.subtract = function( v ) {
  this.x -= v.x;
  this.y -= v.y;
};

Vector.prototype.scale = function( s )  {
  this.x *= s;
  this.y *= s;
};

Vector.prototype.multiply = function( v ) {
  this.x *= v.x;
  this.y *= v.y;
};

// custom getter whaaaaaaat
Object.defineProperty( Vector.prototype, 'magnitude', {
  get: function() {
    return Math.sqrt( this.x * this.x  + this.y * this.y );
  }
});

Vector.prototype.equals = function ( v ) {
  return this.x == v.x && this.y == v.y;
};

Vector.prototype.zero = function() {
  this.x = 0;
  this.y = 0;
};

Vector.prototype.block = function( size ) {
  this.x = Math.floor( this.x / size );
  this.y = Math.floor( this.y / size );
};

Object.defineProperty( Vector.prototype, 'angle', {
  get: function() {
    return normalizeAngle( Math.atan2( this.y, this.x ) );
  }
});

// ----- class functions ----- //
// return new vectors

Vector.subtract = function( a, b ) {
  return new Vector( a.x - b.x, a.y - b.y );
};

Vector.add = function( a, b ) {
  return new Vector( a.x + b.x, a.y + b.y );
};

Vector.copy = function( v ) {
  return new Vector( v.x, v.y );
};

Vector.isSame = function( a, b ) {
  return a.x == b.x && a.y == b.y;
};

Vector.getDistance = function( a, b ) {
  var dx = a.x - b.x;
  var dy = a.y - b.y;
  return Math.sqrt( dx * dx + dy * dy );
};

Vector.addDistance = function( vector, distance, angle ) {
  var x = vector.x + Math.cos( angle ) * distance;
  var y = vector.y + Math.sin( angle ) * distance;
  return new Vector( x, y );
};

// --------------------------  -------------------------- //

// -------------------------- Particle -------------------------- //


function Particle( x, y ) {
  this.position = new Vector( x, y );
  this.previousPosition = new Vector( x, y );
}

Particle.prototype.update = function( friction, gravity ) {
  var velocity = Vector.subtract( this.position, this.previousPosition );
  // friction
  velocity.scale( friction );
  this.previousPosition.set( this.position );
  this.position.add( velocity );
  this.position.add( gravity );
};

// --------------------------  -------------------------- //

Particle.prototype.render = function( ctx ) {
  // big circle
  ctx.fillStyle = 'hsla(0, 0%, 10%, 0.5)';
  circle( ctx, this.position.x, this.position.y, 4 );
  // dot
  // ctx.fillStyle = 'hsla(0, 100%, 50%, 0.5)';
  // circle( this.position.x, this.position.y, 5  );
};

function circle( ctx, x, y, radius ) {
  ctx.beginPath();
  ctx.arc( x, y, radius, 0, Math.PI * 2 );
  ctx.fill();
  ctx.closePath();
}

// --------------------------  -------------------------- //

function StickConstraint( particleA, particleB, distance ) {
  this.particleA = particleA;
  this.particleB = particleB;
  if ( distance ) {
    this.distance = distance;
  } else {
    var delta = Vector.subtract( particleA.position, particleB.position );
    this.distance = delta.magnitude;
  }

  this.distanceSqrd = this.distance * this.distance;
}

StickConstraint.prototype.update = function() {
  var delta = Vector.subtract( this.particleA.position, this.particleB.position );
  var mag = delta.magnitude;
  var scale = ( this.distance - mag ) / mag * 0.5;
  delta.scale( scale );
  this.particleA.position.add( delta );
  this.particleB.position.subtract( delta );
};

StickConstraint.prototype.render = function( ctx ) {
  ctx.strokeStyle = 'hsla(200, 100%, 50%, 0.5)';
  ctx.lineWidth = 2;
  line( ctx, this.particleA.position, this.particleB.position );
};


// --------------------------  -------------------------- //

function PinConstraint( particle, position ) {
  this.particle = particle;
  this.position = position;
}

PinConstraint.prototype.update = function() {
  this.particle.position.set( this.position );
};

PinConstraint.prototype.render = function() {};

// --------------------------  -------------------------- //

function SpringAngleConstraint( particleA, particleB, strength, angle ) {
  this.particleA = particleA;
  this.particleB = particleB;
  this.strength = strength;
  if ( angle === undefined ) {
    var delta = Vector.subtract( particleB.position, particleA.position );
    this.angle = delta.angle;
  } else {
    this.angle = angle;
  }
}

SpringAngleConstraint.prototype.update = function() {
  var positionA = this.particleA.position;
  var positionB = this.particleB.position;
  var delta = Vector.subtract( positionB, positionA );
  var deltaAngle = delta.angle;
  var angleDiff = normalizeAngle( this.angle - deltaAngle );
  angleDiff = angleDiff > Math.PI ? angleDiff - Math.PI * 2 : angleDiff;
  var springAngle = deltaAngle + Math.PI / 2;
  var springForce = new Vector( Math.cos( springAngle ), Math.sin( springAngle ) );
  springForce.scale( angleDiff * this.strength * Math.PI * 2 );
  this.particleB.position.add( springForce );
};

SpringAngleConstraint.prototype.render = function( ctx ) {
  var end = Vector.addDistance( this.particleA.position, 50, this.angle );
  ctx.strokeStyle = 'hsla(0, 0%, 50%, 0.5)';
  line( ctx, this.particleA.position, end );
};

// --------------------------  -------------------------- //

function ChainLinkConstraint( particleA, particleB, distance, shiftEase ) {
  this.particleA = particleA;
  this.particleB = particleB;
  this.distance = distance;
  this.distanceSqrd = distance * distance;
  this.shiftEase = shiftEase === undefined ? 0.85 : shiftEase;
}

ChainLinkConstraint.prototype.update = function() {
  var delta = Vector.subtract( this.particleA.position, this.particleB.position );
  var deltaMagSqrd = delta.x * delta.x + delta.y * delta.y;

  if ( deltaMagSqrd <= this.distanceSqrd ) {
    return;
  }
  var newPosition = Vector.addDistance( this.particleA.position, this.distance, delta.angle + Math.PI );
  var shift = Vector.subtract( newPosition, this.particleB.position );
  shift.scale( this.shiftEase );
  this.particleB.previousPosition.add( shift );
  this.particleB.position.set( newPosition );
};

// --------------------------  -------------------------- //

function Ribbon( props ) {
  extend( this, props );

  // create particles

  this.particles = [];
  this.constraints = [];
  
  this.controlParticle = new Particle( this.controlPoint.x, this.controlPoint.y );
  var pin = new PinConstraint( this.controlParticle, this.controlPoint );
  this.constraints.push( pin );

  var x = this.controlPoint.x;
  for ( var i=0; i < this.sections; i++ ) {
    var y = this.controlPoint.y + this.sectionLength * i;
    var particle = new Particle( x, y );
    this.particles.push( particle );
    // create links
    var linkParticle = i === 0 ? this.controlParticle : this.particles[ i-1 ];
    var link = new ChainLinkConstraint( linkParticle, particle, this.sectionLength, this.chainLinkShiftEase );
    this.constraints.push( link );
  }
}

Ribbon.prototype.update = function() {
  var i, len;
  for ( i=0, len = this.particles.length; i < len; i++ ) {
    this.particles[i].update( this.friction, this.gravity );
  }

  for ( i=0, len = this.constraints.length; i < len; i++ ) {
    this.constraints[i].update();
  }
  for ( i=0, len = this.constraints.length; i < len; i++ ) {
    this.constraints[i].update();
  }
};

Ribbon.prototype.addBreeze = function( v ) {
  for ( var i=0, len = this.particles.length; i < len; i++ ) {
    this.particles[i].position.add( v );
  }
};

Ribbon.prototype.render = function( ctx ) {
  ctx.strokeStyle = '#d916d9';
  ctx.lineWidth = this.width;
  ctx.lineCap = 'butt';
  ctx.lineJoin = 'round';

  ctx.beginPath();
  ctx.moveTo( this.controlParticle.x, this.controlParticle.y );
  for ( var i=0, len = this.particles.length; i < len; i++ ) {
    var particle = this.particles[i];
    ctx.lineTo( particle.position.x, particle.position.y );
  }
  ctx.stroke();
  ctx.closePath();
  ctx.lineWidth = 1;
};

// --------------------------  -------------------------- //

// x, y
// angle
// springStrength
// curl
// segmentLength
// friction
// gravity
// movementStrength
function Follicle( props ) {
  extend( this, props );
  delete this.x;
  delete this.y;
  this.particleA = new Particle( props.x, props.y );
  var positionB = Vector.addDistance( this.particleA.position, this.segmentLength, this.angle );
  this.particleB = new Particle( positionB.x, positionB.y );
  this.stick0 = new StickConstraint( this.particleA, this.particleB );
  this.springAngle0 = new SpringAngleConstraint( this.particleA, this.particleB, this.springStrength, this.angle );

  var angle1 = this.angle + this.curl;
  var positionC =  Vector.addDistance( this.particleB.position, this.segmentLength, angle1 );
  this.particleC = new Particle( positionC.x, positionC.y );
  this.stick1 = new StickConstraint( this.particleB, this.particleC );
  this.springAngle1 = new SpringAngleConstraint( this.particleB, this.particleC, this.springStrength, angle1 );

  this.controlPoint = new Vector( props.x, props.y );
  this.pin = new PinConstraint( this.particleA, this.controlPoint );
}

Follicle.prototype.update = function() {
  this.particleA.update( this.friction, this.gravity );
  this.particleB.update( this.friction, this.gravity );
  this.particleC.update( this.friction, this.gravity );
  this.stick0.update();
  this.springAngle0.update();
  // update springAngle1's angle
  var delta = Vector.subtract( this.particleB.position, this.particleA.position );
  this.springAngle1.angle = delta.angle + this.curl;

  this.pin.update();
  this.stick1.update();
  this.springAngle1.update();
};

Follicle.prototype.move = function( movement ) {
  movement = Vector.copy( movement );
  this.controlPoint.add( movement );
  movement.scale( this.movementStrength );
  this.particleB.position.add( movement );
  this.particleC.position.add( movement );
  this.particleB.previousPosition.add( movement );
  this.particleC.previousPosition.add( movement );
};

Follicle.prototype.render = function( ctx ) {

  ctx.lineWidth = 46;
  ctx.strokeStyle = '#333';
  ctx.lineCap = 'round';
  ctx.beginPath();
  ctx.moveTo( this.particleA.position.x, this.particleA.position.y );
  ctx.quadraticCurveTo( this.particleB.position.x, this.particleB.position.y,
    this.particleC.position.x, this.particleC.position.y );
  ctx.stroke();
  ctx.closePath();
  // reset line props
  ctx.lineCap = 'butt';
  ctx.lineWidth = 1;
};

// --------------------------  -------------------------- //

var canvas = document.querySelector('canvas');
var ctx = canvas.getContext('2d');
var w = canvas.width;
var h = canvas.height;


// --------------------------  -------------------------- //

// --------------------------  -------------------------- //

var friction = 0.75;
var gravity = new Vector( 0, 0.4 );
var movementStrength = 0.2;
var springStrength = 0.5;

var follicles = [];
var pins = [];

var v = new Vector( 112, 110 );
var follicle1 = new Follicle({
  x: v.x,
  y: v.y,
  segmentLength: 54,
  angle: -1.75,
  curl: 1.17,
  friction: friction,
  gravity: gravity,
  springStrength: springStrength,
  movementStrength: movementStrength
});
follicles.push( follicle1 );


v = new Vector( 140, 100 );
var follicle2 = new Follicle({
  x: v.x,
  y: v.y,
  segmentLength: 63,
  angle: -1.33,
  curl: 1.15,
  friction: friction,
  gravity: gravity,
  springStrength: springStrength,
  movementStrength: movementStrength
});
follicles.push( follicle2 );

v = new Vector( 165, 105 );
var follicle3 = new Follicle({
  x: v.x,
  y: v.y,
  segmentLength: 54,
  angle: -1.05,
  curl: 1.15,
  friction: friction,
  gravity: gravity,
  springStrength: springStrength,
  movementStrength: movementStrength
});
follicles.push( follicle3 );

v = new Vector( 178, 113 );
var follicle4 = new Follicle({
  x: v.x,
  y: v.y,
  segmentLength: 52,
  angle: -0.63,
  curl: 1.15,
  friction: friction,
  gravity: gravity,
  springStrength: springStrength,
  movementStrength: movementStrength
});
follicles.push( follicle4 );

v = new Vector( 185, 130 );
var follicle5 = new Follicle({
  x: v.x,
  y: v.y,
  segmentLength: 46,
  angle: -0.29,
  curl: 1.15,
  friction: friction,
  gravity: gravity,
  springStrength: springStrength,
  movementStrength: movementStrength
});
follicles.push( follicle5 );

v = new Vector( 180, 152 );
var follicle6 = new Follicle({
  x: v.x,
  y: v.y,
  segmentLength: 40,
  angle: 0.05,
  curl: 1.15,
  friction: friction,
  gravity: gravity,
  springStrength: springStrength,
  movementStrength: movementStrength
});
follicles.push( follicle6 );

v = new Vector( 160, 166 );
var follicle7 = new Follicle({
  x: v.x,
  y: v.y,
  segmentLength: 30,
  angle: 0.45,
  curl: 0.8,
  friction: friction,
  gravity: gravity,
  springStrength: springStrength,
  movementStrength: movementStrength
});
follicles.push( follicle7 );

// middle bottom
v = new Vector( 145, 166 );
var follicle8 = new Follicle({
  x: v.x,
  y: v.y,
  segmentLength: 26,
  angle: Math.PI / 2,
  curl: 0,
  friction: friction,
  gravity: gravity,
  springStrength: springStrength,
  movementStrength: movementStrength
});
follicles.push( follicle8 );

// compare to 7
v = new Vector( 130, 166 );
var follicle9 = new Follicle({
  x: v.x,
  y: v.y,
  segmentLength: 30,
  angle: 2.7,
  curl: -0.8,
  friction: friction,
  gravity: gravity,
  springStrength: springStrength,
  movementStrength: movementStrength
});
follicles.push( follicle9 );

// compare to 6
v = new Vector( 118, 152 );
var follicle10 = new Follicle({
  x: v.x,
  y: v.y,
  segmentLength: 46,
  angle: 3.20,
  curl: -1.15,
  friction: friction,
  gravity: gravity,
  springStrength: springStrength,
  movementStrength: movementStrength
});
follicles.push( follicle10 );

// compare to 5
v = new Vector( 105, 130 );
var follicle11 = new Follicle({
  x: v.x,
  y: v.y,
  segmentLength: 46,
  angle: -2.8,
  curl: -1.15,
  friction: friction,
  gravity: gravity,
  springStrength: springStrength,
  movementStrength: movementStrength
});
follicles.push( follicle11 );

// compare to 4
v = new Vector( 120, 105 );
var follicle12 = new Follicle({
  x: v.x,
  y: v.y,
  segmentLength: 52,
  angle: -2.5,
  curl: -1.15,
  friction: friction,
  gravity: gravity,
  springStrength: springStrength,
  movementStrength: movementStrength
});
follicles.push( follicle12 );

// --------------------------  -------------------------- //

var ribbon0 = new Ribbon({
  controlPoint: new Vector( 130, 180 ),
  sections: 30,
  width: 40,
  sectionLength: 8,
  friction: 0.95,
  gravity: new Vector( 0, 0.2 ),
  chainLinkShiftEase: 0.9
});

var ribbon1 = new Ribbon({
  controlPoint: new Vector( 130, 180 ),
  sections: 30,
  width: 40,
  sectionLength: 8,
  friction: 0.9,
  gravity: new Vector( 0, 0.25 ),
  chainLinkShiftEase: 0.9
});

// --------------------------  -------------------------- //

var headImg = new Image();
var isHeadImgLoaded;
headImg.onload = function() {
  isHeadImgLoaded = true;
};
headImg.src = 'https://s3-us-west-2.amazonaws.com/s.cdpn.io/82/cole-run-cycle-head.png';

// --------------------------  -------------------------- //

var origin = new Vector( 300, 300 );
var torso = new Vector();
var previousTorso = new Vector();
var coccyx = new Vector();
var head = new Vector();
var leftShoulder, leftElbow, leftWrist, leftHip, leftKnee, leftAnkle, leftToe;
var rightShoulder, rightElbow, rightWrist, rightHip, rightKnee, rightAnkle, rightToe;
var leftThigh, rightThigh;
var leftShoulderOffset = new Vector( -40, 0 );
var rightShoulderOffset = new Vector( 30, 0 );
var shoulderAmplitude = 10;
var armLength = 35;
var leftHipOffset = new Vector( -40, 48 );
var rightHipOffset = new Vector( 10, 48 );
var hipAmplitude = 10;
var legLength = 30;
var footLength = 30;


// --------------------------  -------------------------- //

var cycleTheta = 0;
var cycleSpeed = 0.07;
var PI = Math.PI;
var TAU = PI * 2;

var breeze = new Vector( -0.5, 0 );

function update() {
  previousTorso.set( torso );
  updateCycle();

  var movement = previousTorso.x === 0 ? new Vector() : Vector.subtract( torso, previousTorso );

  ribbon0.controlPoint.add( movement );
  ribbon1.controlPoint.add( movement );
  ribbon0.addBreeze( breeze );
  ribbon1.addBreeze( breeze );
  ribbon0.update();
  ribbon1.update();
  var i, len;
  for ( i=0, len = follicles.length; i < len; i++ ) {
    follicles[i].move( movement );
    follicles[i].update();
  }
  for ( i=0, len = pins.length; i < len; i++ ) {
    pins[i].update();
  }
  for ( i=0, len = follicles.length; i < len; i++ ) {
    follicles[i].stick0.update();
    follicles[i].stick1.update();
  }
}

function updateCycle() {
  cycleTheta += cycleSpeed;

  var sin = Math.sin( cycleTheta );

  torso.set( origin );
  var lift = Math.cos( cycleTheta - 1 );
  torso.y -= Math.abs( lift ) * 40;
  // torso.y -= Math.max( 0, Math.cos( cycleTheta * 2 - 2 ) ) * 20;
  coccyx.set( torso );
  coccyx.y += leftHipOffset.y;
  head.set( torso );
  head.y -= 30;

  // shoulder
  var quadFactor = 1.5;
  leftShoulder = Vector.add( torso, leftShoulderOffset );
  var quadSine = sin > 0 ? quadWave( sin, quadFactor ) : sin;
  // var normTheta = normalizeAngle( cycleTheta );
  // var quadSine = Math.floor( cycleTheta / ( TAU/4) ) % 2 ?  quadWave( sin, squareFactor ) : sin;
  leftShoulder.x += quadSine * shoulderAmplitude;
  // elbow
  var leftElbowAngle = -quadSine * 1.0 + 1.7;
  leftElbow = Vector.addDistance( leftShoulder, armLength, leftElbowAngle );
  // wrist

  var leftWristAngle = leftElbowAngle - quadSine * 0.4 - PI / 2;
  leftWrist = Vector.addDistance( leftElbow, armLength, leftWristAngle );
  // hip
  leftHip = Vector.add( torso, leftHipOffset );
  leftHip.x += -quadSine * hipAmplitude;
  // knee
  var leftKneeAngle = quadSine * 0.9 + 1.5;
  leftKnee = Vector.addDistance( leftHip, legLength, leftKneeAngle );
  leftThigh = Vector.addDistance( leftHip, legLength/2, leftKneeAngle );
  // ankle
  var ankleTheta = cycleTheta - TAU/8;
  var normAnkleTheta = normalizeAngle( ankleTheta );
  var ankleAngle = Math.max( 0, Math.sin( normAnkleTheta * 2/3 ) ) * 2 - 1;
  var leftAnkleAngle = ( ankleAngle + 1 ) * 0.75;
  leftAnkleAngle += leftKneeAngle;
  leftAnkle = Vector.addDistance( leftKnee, legLength, leftAnkleAngle );
  leftToe = Vector.addDistance( leftAnkle, footLength, leftAnkleAngle - TAU/4 );
  // right
  // shoulder
  quadSine = sin < 0 ? quadWave( sin, quadFactor ) : sin;
  rightShoulder = Vector.add( torso, rightShoulderOffset );
  rightShoulder.x += quadSine * shoulderAmplitude * -1;
  // elbow
  var rightElbowAngle = quadSine * 1.0 + 1.7;
  rightElbow = Vector.addDistance( rightShoulder, armLength, rightElbowAngle );
  // wrist
  var rightWristAngle = rightElbowAngle + quadSine * 0.4 - PI / 2;
  rightWrist = Vector.addDistance( rightElbow, armLength, rightWristAngle );
  // hip
  rightHip = Vector.add( torso, rightHipOffset );
  rightHip.x += quadSine * hipAmplitude;
  // knee
  var rightKneeAngle = -quadSine * 0.9 + 1.5;
  rightKnee = Vector.addDistance( rightHip, legLength, rightKneeAngle );
  rightThigh = Vector.addDistance( rightHip, legLength/2, rightKneeAngle );
  // ankle
  ankleTheta = cycleTheta - TAU/8 + TAU/2;
  normAnkleTheta = normalizeAngle( ankleTheta );
  ankleAngle = Math.max( 0, Math.sin( normAnkleTheta * 2/3 ) ) * 2 - 1;
  var rightAnkleAngle = ( ankleAngle + 1 ) * 0.75;
  rightAnkleAngle += rightKneeAngle;
  rightAnkle = Vector.addDistance( rightKnee, legLength, rightAnkleAngle );
  rightToe = Vector.addDistance( rightAnkle, footLength, rightAnkleAngle - TAU/4 );
}

var scale = 1;

function render() {
  ctx.clearRect( 0, 0, w, h );

  ctx.save();
  ctx.scale( scale, scale );
  ctx.save();
  ctx.translate( 150, 50 );
  ribbon0.render( ctx );
  ribbon1.render( ctx );

  for ( var i=0, len = follicles.length; i < len; i++ ) {
    follicles[i].render( ctx );
  }

  ctx.restore();
  renderIllo();
  ctx.restore();
  // renderSkeleton();
}

var brownSkin = '#A74';
var black = '#333';
var magenta = '#B1B';

function renderIllo() {
  // right arm
  renderIlloArm( rightShoulder, rightElbow, rightWrist, true );
  // right leg
  renderIlloLeg( rightHip, rightKnee, rightAnkle, rightThigh, rightToe, black );
  // torso bottom
  ctx.fillStyle = black;
  ctx.beginPath();
  ctx.arc( leftHip.x + 15, leftHip.y - 15, 42, TAU/4, -TAU/4 );
  ctx.fill();
  ctx.closePath();

  ctx.beginPath();
  ctx.arc( rightHip.x - 15, rightHip.y - 15, 42, -TAU/4, TAU/4 );
  ctx.fill();
  ctx.closePath();
  ctx.fillRect( leftHip.x + 14.5, torso.y - 9, ( rightHip.x - 14.5) - (leftHip.x + 14.5), 84 );
  // torso top
  ctx.fillStyle = black;
  fillCircle( Vector.add( torso, { x: -33, y: 0 } ), 25 );
  fillCircle( Vector.add( torso, { x: 18, y: 0 } ), 25 );
  // fillCircle( Vector.add( rightShoulder, { x: -15, y: 0 } ), 25 );
  ctx.fillRect( torso.x - 33, torso.y - 25, 56, 50 );
  // head
  if ( isHeadImgLoaded ) {
    ctx.drawImage( headImg, torso.x - 70, torso.y - 145 );
  }
  // left leg
  renderIlloLeg( leftHip, leftKnee, leftAnkle, leftThigh, leftToe, magenta );
  // left arm
  renderIlloArm( leftShoulder, leftElbow, leftWrist );
}

function renderIlloArm( shoulder, elbow, wrist, hasBand ) {
  ctx.strokeStyle = brownSkin;
  ctx.lineWidth = 45;
  ctx.lineCap = 'round';
  ctx.lineJoin = 'round';

  ctx.beginPath();
  ctx.moveTo( shoulder.x, shoulder.y );
  ctx.lineTo( elbow.x, elbow.y );
  ctx.lineTo( wrist.x, wrist.y );
  ctx.stroke();
  ctx.closePath();

  if ( hasBand ) {
    ctx.strokeStyle = magenta;
    ctx.beginPath();
    ctx.moveTo( elbow.x, elbow.y );
    ctx.lineTo( wrist.x, wrist.y );
    ctx.stroke();
    ctx.closePath();
  }

  // ctx.fillStyle = !hasBand ? magenta : brownSkin;
  ctx.fillStyle = brownSkin;
  fillCircle( wrist, 28 );
}

function fillCircle( v, radius ) {
  ctx.beginPath();
  ctx.arc( v.x, v.y, radius, 0, TAU );
  ctx.fill();
  ctx.closePath();
}

function renderIlloLeg( hip, knee, ankle, thigh, toe, footColor ) {
  ctx.lineWidth = 45;
  ctx.lineCap = 'round';
  ctx.lineJoin = 'round';

  ctx.strokeStyle = black;
  ctx.beginPath();
  ctx.moveTo( hip.x, hip.y );
  ctx.lineTo( knee.x, knee.y );
  ctx.lineTo( ankle.x, ankle.y );
  ctx.stroke();
  ctx.closePath();

  // foot
  ctx.lineCap = 'round';
  ctx.lineWidth = 50;
  ctx.strokeStyle = magenta;
  ctx.beginPath();
  ctx.moveTo( ankle.x, ankle.y );
  ctx.lineTo( toe.x, toe.y );
  ctx.stroke();
  ctx.closePath();
}

function dot( ctx, v ) {
  ctx.beginPath();
  ctx.arc( v.x, v.y, 6, 0, Math.PI * 2 );
  ctx.fill();
  ctx.closePath();
}

// i: sin or cos
// b: square factor
function quadWave( i, b ) {
  return Math.sqrt( ( 1 + b * b ) / ( 1 + b * b * i * i ) ) * i;
}


var isAnimating = false;

function animate() {
  update();
  render();
  requestAnimationFrame( animate );
}

function start() {
  isAnimating = true;
}

// --------------------------  -------------------------- //

animate();

var speedRange = document.querySelector('.speed-range')
speedRange.onchange = function() {
  cycleSpeed = parseFloat( speedRange.value );
};

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.