Skip to content

Instantly share code, notes, and snippets.

@xav76
Created October 23, 2012 18:57
Show Gist options
  • Select an option

  • Save xav76/3940815 to your computer and use it in GitHub Desktop.

Select an option

Save xav76/3940815 to your computer and use it in GitHub Desktop.
A CodePen by Justin Windle. Worms - Experimenting with Inverse kinematics, Catmull-Rom Splines & Wander Behaviors
<section id="info">
<header>
<hgroup>
<h1>Worms</h1>
<h2>Inverse kinematics, Catmull-Rom Splines &amp; Wander Behaviors</h2>
</hgroup>
<a href="https://github.com/soulwire/Worms/zipball/master" target="_blank">Download</a>
<a href="https://github.com/soulwire/Worms" target="_blank">View on Github</a>
</header>
</section>
<div id="container"></div>
// ----------------------------------------
// Config
// ----------------------------------------
var Config = {
COLORS: [
[349, 99, 63], // #FE4365
[25, 86, 83], // #F9CDAE
[177, 42, 76], // #A8DCD9
[350, 65, 46], // #C02A43
[185, 19, 40], // #537679
[46, 75, 70], // #ECD179
[153, 22, 60] // #83AF9B
],
SHADOW: true,
NUM_WORMS: 40,
MIN_LENGTH: 5,
MAX_LENGTH: 35,MIN_THICKNESS: 12,
MAX_THICKNESS: 20,
MIN_SEGMENT_SPACING: 1,
MAX_SEGMENT_SPACING: 4
};
// ----------------------------------------
// Catmull Rom Spline
// ----------------------------------------
var CatmullRom = (function() {
var p0, p1, p2, p3, i6 = 1.0 / 6.0;
return {
curveThroughPoints: function( points, ctx ) {
for ( var i = 3, n = points.length; i < n; i++ ) {
p0 = points[i - 3];
p1 = points[i - 2];
p2 = points[i - 1];
p3 = points[i];
ctx.bezierCurveTo(
p2.x * i6 + p1.x - p0.x * i6,
p2.y * i6 + p1.y - p0.y * i6,
p3.x * -i6 + p2.x + p1.x * i6,
p3.y * -i6 + p2.y + p1.y * i6,
p2.x,
p2.y
);
}
}
};
})();
// ----------------------------------------
// Vector
// ----------------------------------------
function Vector( x, y ) {
this.set( x, y );
}
Vector.add = function( a, b ) {
return new Vector( a.x + b.x, a.y + b.y );
};
Vector.sub = function( a, b ) {
return new Vector( a.x - b.x, a.y - b.y );
};
Vector.scale = function( v, f ) {
return v.clone().scale( f );
};
Vector.random = function() {
return new Vector(
2 * ( Math.random() - 0.5 ),
2 * ( Math.random() - 0.5 ));
},
Vector.prototype = {
set: function( x, y ) {
this.x = x || 0.0;
this.y = y || 0.0;
return this;
},
add: function( v ) {
this.x += v.x;
this.y += v.y;
return this;
},
sub: function( v ) {
this.x -= v.x;
this.y -= v.y;
return this;
},
div: function( v ) {
this.x /= v.x;
this.y /= v.y;
return this;
},
mag: function() {
return Math.sqrt( this.x * this.x + this.y * this.y );
},
magSq: function() {
return this.x * this.x + this.y * this.y;
},
normalize: function() {
var mag = Math.sqrt( this.x * this.x + this.y * this.y ) + 0.000001;
this.x /= mag;
this.y /= mag;
return this;
},
angle: function() {
return Math.atan2( this.y, this.x );
},
lookAt: function( v ) {
var mag = this.mag();
var dx = v.x - this.x;
var dy = v.y - this.y;
var theta = Math.atan2( dy, dx );
this.rotate( theta - this.angle() );
this.normalize();
this.scale( mag );
},
rotate: function( theta ) {
var s = sin( theta );
var c = cos( theta );
var x = this.x * c - this.y * s;
var y = this.x * s + this.y * c;
this.x = x;
this.y = y;
return this;
},
scale: function( f ) {
this.x *= f;
this.y *= f;
return this;
},
copy: function( v ) {
this.set( v.x, v.y );
return this;
},
clone: function() {
return new Vector( this.x, this.y );
}
};
// ----------------------------------------
// Segment
// ----------------------------------------
function Segment( size, head, tail ) {
this.size = size;
this.head = head || new Vector();
this.tail = tail || new Vector( this.head.x + size, this.head.y + size );
}
Segment.prototype = {
update: function() {
// Position derivitives
var dx = this.head.x - this.tail.x;
var dy = this.head.y - this.tail.y;
var dist = Math.sqrt( dx * dx + dy * dy );
var force = 0.5 - this.size / dist * 0.5;
var strength = 0.998; // No springiness
force *= 0.99;
var fx = force * dx;
var fy = force * dy;
this.tail.x += fx * strength * 2.0;
this.tail.y += fy * strength * 2.0;
this.head.x -= fx * ( 1.0 - strength ) * 2.0;
this.head.y -= fy * ( 1.0 - strength ) * 2.0;
}
};
// ----------------------------------------
// Worm
// ----------------------------------------
function Worm( length, thickness, spacing, color ) {
this.thickness = thickness || 8;
this.segments = [];
this.spacing = spacing || 5;
this.length = length || 10;
this.color = color || '#333333';
this.nodes = [];
this.head = null;
this.tail = null;
this.meander = new Vector( 1.0, 1.0 );
this.jitter = random( 0.2, 1.5 );
this.maxForce = random( 0.08, 0.15 );
this.velocity = new Vector();
this.force = new Vector();
this.create();
}
Worm.prototype = {
create: function() {
// Build segments
var node = new Vector();
var theta;
var radius;
var segment;
this.nodes.push( node );
for ( var i = 0; i < this.length; i++ ) {
segment = new Segment( this.spacing, node );
node = segment.tail;
theta = i * random( 0.2, 0.6 );
radius = random( 20, 40 );
node.x += sin( theta ) * radius;
node.y += cos( theta ) * radius;
this.segments.push( segment );
this.nodes.push( node );
}
this.head = this.nodes[0];
this.tail = this.nodes[ this.nodes.length - 1 ];
// Start with a random direction
this.meander.rotate( random( -PI, PI ) );
},
setSpacing: function( spacing ) {
this.spacing = spacing;
for ( var i = 0, n = this.segments.length; i < n; i++ ) {
this.segments[i].size = spacing;
}
},
update: function() {
// Integrate movement
this.force.normalize();
this.force.scale( this.maxForce );
this.velocity.add( this.force );
this.velocity.scale( 0.985 );
this.head.add( this.velocity );
// Reset force
this.force.set();
// Inverse Kinematics step
for ( var i = 0, n = this.segments.length; i < n; i++ ) {
this.segments[i].update();
}
},
wander: function( multiplier ) {
this.meander.rotate( random( -this.jitter, this.jitter ) );
this.force.add( Vector.scale( this.meander, multiplier || 1.0 ) );
},
seek: function( target, multiplier ) {
var desired = Vector.sub( target, this.head );
var distance = desired.magSq();
if ( distance > 0.00001 ) {
var steer = Vector.sub( desired, this.velocity );
steer.scale( multiplier || 1.0 );
this.force.add( steer );
}
},
flee: function( target, multiplier ) {
var desired = Vector.sub( target, this.head );
var distance = desired.magSq();
if ( distance > 0.00001 && distance < 100 ) {
var steer = Vector.sub( this.velocity, desired );
steer.scale( multiplier || 1.0 );
this.force.add( steer );
}
},
getBounds: function() {
var radius = this.thickness * 0.5;
var node;
var minX = 999999;
var minY = 999999;
var maxX = -999999;
var maxY = -999999;
for ( var i = 0, n = this.nodes.length; i < n; i++ ) {
node = this.nodes[i];
minX = min( minX, node.x - radius );
minY = min( minY, node.y - radius );
maxX = max( maxX, node.x + radius );
maxY = max( maxY, node.y + radius );
}
return {
x1: minX,
y1: minY,
x2: maxX,
y2: maxY
};
},
draw: function( ctx ) {
if ( Config.SHADOW ) {
ctx.beginPath();
CatmullRom.curveThroughPoints( this.nodes, ctx );
ctx.strokeStyle = 'rgba(0,0,0,0.1)';
ctx.lineWidth = this.thickness + 8;
ctx.lineJoin = 'round';
ctx.lineCap = 'round';
ctx.stroke();
}
// Draw
ctx.beginPath();
CatmullRom.curveThroughPoints( this.nodes, ctx );
ctx.strokeStyle = 'hsl(' + this.color[0] + ',' + this.color[1] + '%,' + this.color[2] + '%)';
ctx.lineWidth = this.thickness;
ctx.lineJoin = 'round';
ctx.lineCap = 'round';
ctx.stroke();
},
drawBounds: function( ctx ) {
// Debug draw bounds
var bounds = this.getBounds();
ctx.beginPath();
ctx.moveTo( bounds.x1, bounds.y1 );
ctx.lineTo( bounds.x2, bounds.y1 );
ctx.lineTo( bounds.x2, bounds.y2 );
ctx.lineTo( bounds.x1, bounds.y2 );
ctx.lineTo( bounds.x1, bounds.y1 );
ctx.strokeStyle = '#ff0000';
ctx.lineWidth = 1;
ctx.stroke();
}
};
// ----------------------------------------
// Sketch
// https://github.com/soulwire/sketch.js
// ----------------------------------------
$(function() {
var worms = [];
var mouse = new Vector();
var center = new Vector();
var ctx = Sketch.create({
container: document.getElementById( 'container' ),
interval: 2,
setup: function() {
center.set( this.width * 0.5, this.height * 0.5 );
var length, offset, scale, color, worm;
for ( var i = 0; i < Config.NUM_WORMS; i++ ) {
length = random( Config.MIN_LENGTH, Config.MAX_LENGTH );
thickness = length * random( 0.5, 1.5 );
color = Config.COLORS[ i % Config.COLORS.length ].concat();
// Vary Config.HSL
color[0] += random( -10, 10 );
color[1] += random( -10, 10 );
color[2] += random( -10, 10 );
worm = new Worm(
length,
thickness,
random( Config.MIN_SEGMENT_SPACING, Config.MAX_SEGMENT_SPACING ),
color
);
// Position the worm around the center
offset = new Vector( random( -300, 300 ), random( -100, 100 ) )
worm.head.copy( center.clone().add( offset ) );
// Initial wander
for ( var j = 0; j < 60; j++ ) {
worm.wander();
worm.update();
}
worms.push( worm );
}
},
refresh: function() {
var worm, spacing;
for ( var i = 0; i < Config.NUM_WORMS; i++ ) {
worm = worms[i];
worm.setSpacing( random( Config.MIN_SEGMENT_SPACING, Config.MAX_SEGMENT_SPACING ) );
worm.thickness = random( Config.MIN_THICKNESS, Config.MAX_THICKNESS );
}
},
update: function() {
var worm;
var bounds;
for ( var i = worms.length - 1; i >= 0; i-- ) {
worm = worms[i];
// Apply behaviours
worm.wander();
// Move towards center if our of bounds
bounds = worm.getBounds();
if ( bounds.x2 < 0 || bounds.x1 > this.width || bounds.y2 < 0 || bounds.y1 > this.height ) {
worm.seek( center );
}
worm.update();
}
},
draw: function() {
ctx.globalAlpha = 0.92;
for ( var i = worms.length - 1; i >= 0; i-- ) {
worms[i].draw( ctx );
}
},
toggle: function() {
if ( ctx.running ) ctx.stop();
else ctx.start();
},
resize: function() {
center.set( this.width * 0.5, this.height * 0.5 );
},
mousemove: function( e ) {
mouse.set( e.x, e.y );
}
});
// ----------------------------------------
// GUI
// ----------------------------------------
var gui = new DAT.GUI();
gui.add( Config, 'MAX_THICKNESS' ).min(1).max(80).name( 'Thickness' );
gui.add( Config, 'MAX_SEGMENT_SPACING' ).min(1).max(20).name( 'Length' );
gui.add( Config, 'SHADOW' ).name( 'Shadow' );
gui.add( ctx, 'toggle' ).name( 'Start / Stop' );
gui.add( ctx, 'refresh' ).name( 'Update' );
gui.close();
});
* {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
html, body {
background: #a90329; /* Old browsers */
background: -moz-radial-gradient(center, ellipse cover, #a90329 0%, #8f0222 44%, #6d0019 100%); /* FF3.6+ */
background: -webkit-gradient(radial, center center, 0px, center center, 100%, color-stop(0%,#a90329), color-stop(44%,#8f0222), color-stop(100%,#6d0019)); /* Chrome,Safari4+ */
background: -webkit-radial-gradient(center, ellipse cover, #a90329 0%,#8f0222 44%,#6d0019 100%); /* Chrome10+,Safari5.1+ */
background: -o-radial-gradient(center, ellipse cover, #a90329 0%,#8f0222 44%,#6d0019 100%); /* Opera 12+ */
background: -ms-radial-gradient(center, ellipse cover, #a90329 0%,#8f0222 44%,#6d0019 100%); /* IE10+ */
background: radial-gradient(ellipse at center, #a90329 0%,#8f0222 44%,#6d0019 100%); /* W3C */
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#a90329', endColorstr='#6d0019',GradientType=1 ); /* IE6-9 fallback on horizontal gradient */
overflow: hidden;
margin: 0;
}
/* Info */
#info {
font-size: 12px;
position: absolute;
z-index: 1;
color: #fff;
width: 220px;
left: 20px;
top: 20px;
}
#info a {
text-decoration: none;
border-bottom: 1px dotted rgba(255, 255, 255, 0.2);
color: rgba(255, 255, 255, 0.5);
}
#info a:hover {
color: #fff;
}
#info header, #info aside {
font-family: 'Play', sans-serif;
background: rgba(0, 0, 0, 0.8);
margin-bottom: 1px;
}
#info header {
padding: 12px 10px;
}
#info header hgroup h1 {
line-height: 22px;
font-weight: 300;
font-size: 18px;
margin: 0;
}
#info header hgroup h2 {
line-height: 14px;
font-weight: 300;
font-size: 12px;
color: rgba(255, 255, 255, 0.8);
margin: 0 0 6px 0;
}
#info header a {
text-transform: uppercase;
margin-right: 4px;
line-height: 20px;
font-size: 10px;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment