Created
July 11, 2012 03:16
-
-
Save CWSpear/3087756 to your computer and use it in GitHub Desktop.
A web page created at CodePen.io
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html> | |
<head> | |
<meta charset="UTF-8"> | |
<title>Blob · CodePen</title> | |
<!-- | |
Copyright (c) 2012 Hakim El Hattab, http://codepen.io/hakimel | |
Permission is hereby granted, free of charge, to any person obtaining | |
a copy of this software and associated documentation files (the | |
"Software"), to deal in the Software without restriction, including | |
without limitation the rights to use, copy, modify, merge, publish, | |
distribute, sublicense, and/or sell copies of the Software, and to | |
permit persons to whom the Software is furnished to do so, subject to | |
the following conditions: | |
The above copyright notice and this permission notice shall be | |
included in all copies or substantial portions of the Software. | |
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, | |
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF | |
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND | |
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE | |
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION | |
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION | |
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | |
--> | |
<style> | |
body { | |
background-color: #333333; | |
padding: 0; | |
margin: 0; | |
overflow: hidden; | |
} | |
p { | |
position: absolute; | |
z-index: 99; | |
color: #cccccc; | |
margin: 2px; | |
padding: 0; | |
font-family: Arial; | |
font-size: 10px; | |
} | |
a { | |
color: #f4f4f4; | |
font-weight: bold; | |
} | |
</style> | |
<style> | |
#codepen-footer, #codepen-footer * { | |
-webkit-box-sizing: border-box !important; | |
-moz-box-sizing: border-box !important; | |
box-sizing: border-box !important; | |
} | |
#codepen-footer { | |
display: block !important; | |
position: fixed !important; | |
bottom: 0 !important; | |
left: 0 !important; | |
width: 100% !important; | |
padding: 0 10px !important; | |
margin: 0 !important; | |
height: 30px !important; | |
line-height: 30px !important; | |
font-size: 12px !important; | |
color: #eeeeee !important; | |
background-color: #505050 !important; | |
text-align: left !important; | |
background: -webkit-linear-gradient(top, #505050, #383838) !important; | |
background: -moz-linear-gradient(top, #505050, #383838) !important; | |
background: -ms-linear-gradient(top, #505050, #383838) !important; | |
background: -o-linear-gradient(top, #505050, #383838) !important; | |
border-top: 1px solid black !important; | |
border-bottom: 1px solid black !important; | |
border-radius: 0 !important; | |
border-image: none !important; | |
box-shadow: inset 0 1px 0 #6e6e6e, 0 2px 2px rgba(0, 0, 0, 0.4) !important; | |
z-index: 300 !important; | |
font-family: "Lucida Grande", "Lucida Sans Unicode", Tahoma, sans-serif !important; | |
letter-spacing: 0 !important; | |
word-spacing: 0 !important; | |
} | |
#codepen-footer a { | |
color: #a7a7a7 !important; | |
text-decoration: none !important; | |
} | |
#codepen-footer a:hover { | |
color: white !important; | |
} | |
</style> | |
<script> | |
// Kill alerts, confirmations and prompts | |
window.alert = function(){}; | |
window.confirm = function(){}; | |
window.prompt = function(){}; | |
window.open = function(){}; | |
window.print = function(){}; | |
</script> | |
</head> | |
<body> | |
<p>Double click to split. <a id="keyboardUp" href="#">Increase</a> / <a id="keyboardDown" href="#">decrease</a> size or <a id="keyboardLeft" href="#">Previous</a> / <a id="keyboardRight" href="#">Next</a> skin.</p> | |
<canvas id='world'></canvas> | |
<script> | |
(function() { | |
/** | |
* Best seen in fullscreen: http://hakim.se/experiments/html5/blob/03/ | |
*/ | |
BlobWorld = new function() { | |
var SCREEN_WIDTH = window.innerWidth; | |
var SCREEN_HEIGHT = window.innerHeight; | |
var canvas; | |
var context; | |
var blobs = []; | |
var dragBlob; | |
var screenX = window.screenX; | |
var screenY = window.screenY; | |
var mouseX = (window.innerWidth - SCREEN_WIDTH); | |
var mouseY = (window.innerHeight - SCREEN_HEIGHT); | |
var mouseIsDown = false; | |
var mouseDownOffset = { x: 0, y: 0 }; | |
// The bounds of the world | |
var worldRect = { x: 0, y: 0, width: 0, height: 0 }; | |
// The world gravity, applied to all blobs | |
var gravity = { x: 0, y: 1.2 }; | |
// A pair of blobs that should be merged | |
var mergeQueue = { blobA: -1, blobB: -1 }; | |
var skinIndex = 0; | |
var skins = [ | |
{ fillStyle: 'rgba(0,200,250,1.0)', strokeStyle: '#ffffff', lineWidth: 5, debug: false }, | |
{ fillStyle: '', strokeStyle: '', lineWidth: 0, debug: true }, | |
{ fillStyle: 'rgba(0,0,0,0.1)', strokeStyle: 'rgba(255,255,255,1.0)', lineWidth: 6, debug: false }, | |
{ fillStyle: 'rgba(0,230,110,1.0)', strokeStyle: 'rgba(0,0,0,1.0)', lineWidth: 2, debug: false }, | |
{ fillStyle: 'rgba(255,255,0,1.0)', strokeStyle: 'rgba(0,0,0,1.0)', lineWidth: 4, debug: false }, | |
{ fillStyle: 'rgba(255,255,255,1.0)', strokeStyle: 'rgba(0,0,0,1.0)', lineWidth: 4, debug: false } | |
]; | |
this.init = function() { | |
canvas = document.getElementById( 'world' ); | |
if (canvas && canvas.getContext) { | |
context = canvas.getContext('2d'); | |
// Register event listeners | |
document.addEventListener('mousemove', documentMouseMoveHandler, false); | |
canvas.addEventListener('mousedown', documentMouseDownHandler, false); | |
canvas.addEventListener('dblclick', documentDoubleClickHandler, false); | |
document.addEventListener('mouseup', documentMouseUpHandler, false); | |
document.addEventListener('touchstart', documentTouchStartHandler, false); | |
document.addEventListener('touchmove', documentTouchMoveHandler, false); | |
document.addEventListener('touchend', documentTouchEndHandler, false); | |
document.addEventListener('keydown', documentKeyDownHandler, false); | |
window.addEventListener('resize', windowResizeHandler, false); | |
document.getElementById( 'keyboardUp' ).addEventListener('click', keyboardUpHandler, false); | |
document.getElementById( 'keyboardDown' ).addEventListener('click', keyboardDownHandler, false); | |
document.getElementById( 'keyboardLeft' ).addEventListener('click', keyboardLeftHandler, false); | |
document.getElementById( 'keyboardRight' ).addEventListener('click', keyboardRightHandler, false); | |
createBlob( { x: SCREEN_WIDTH*0.5, y: SCREEN_HEIGHT*0.1 } ); | |
windowResizeHandler(); | |
setInterval( loop, 1000 / 60 ); | |
} | |
}; | |
function createBlob( position ) { | |
var blob = new Blob(); | |
blob.position.x = position.x; | |
blob.position.y = position.y; | |
blob.generateNodes(); | |
blobs.push( blob ); | |
} | |
function splitBlob( blob ) { | |
if( blob.quality > 8 ) { | |
blobs.push( blob.split() ); | |
} | |
} | |
function mergeBlobs( blobA, blobB ) { | |
var t = getTime(); | |
if( !blobs[blobA] || !blobs[blobB] ) { | |
return; | |
} | |
if( t - blobs[blobA].lastSplitTime > 500 && t - blobs[blobB].lastSplitTime > 500 ) { | |
// Merge blobB with blobA | |
blobs[blobA].merge( blobs[blobB] ); | |
// Remove blobB since blobA will take over its body | |
blobs.splice( blobB, 1 ); | |
} | |
} | |
function documentMouseMoveHandler(event) { | |
mouseX = event.clientX - (window.innerWidth - SCREEN_WIDTH) * .5; | |
mouseY = event.clientY - (window.innerHeight - SCREEN_HEIGHT) * .5; | |
} | |
function documentMouseDownHandler(event) { | |
event.preventDefault(); | |
mouseIsDown = true; | |
dragBlob = blobs[ findClosestBody( blobs, { x: mouseX, y: mouseY } ) ]; | |
var closestNodeIndex = findClosestBody( dragBlob.nodes, { x: mouseX, y: mouseY } ); | |
dragBlob.dragNodeIndex = closestNodeIndex; | |
mouseDownOffset.y = 100; | |
} | |
function documentMouseUpHandler(event) { | |
mouseIsDown = false; | |
if( dragBlob ) { | |
dragBlob.dragNodeIndex = -1; | |
dragBlob = null; | |
} | |
} | |
function documentTouchStartHandler(event) { | |
if(event.touches.length == 1) { | |
event.preventDefault(); | |
mouseIsDown = true; | |
mouseX = event.touches[0].pageX - (window.innerWidth - SCREEN_WIDTH) * .5; | |
mouseY = event.touches[0].pageY - (window.innerHeight - SCREEN_HEIGHT) * .5; | |
dragBlob = blobs[ findClosestBody( blobs, { x: mouseX, y: mouseY } ) ]; | |
var closestNodeIndex = findClosestBody( dragBlob.nodes, { x: mouseX, y: mouseY } ); | |
dragBlob.dragNodeIndex = closestNodeIndex; | |
mouseDownOffset.y = 100; | |
} | |
} | |
function documentTouchMoveHandler(event) { | |
if(event.touches.length == 1) { | |
event.preventDefault(); | |
mouseX = event.touches[0].pageX - (window.innerWidth - SCREEN_WIDTH) * .5; | |
mouseY = event.touches[0].pageY - (window.innerHeight - SCREEN_HEIGHT) * .5; | |
} | |
} | |
function documentTouchEndHandler(event) { | |
mouseIsDown = false; | |
if( dragBlob ) { | |
dragBlob.dragNodeIndex = -1; | |
dragBlob = null; | |
} | |
} | |
function documentDoubleClickHandler(event) { | |
var mouse = { x: mouseX, y: mouseY }; | |
var blob = blobs[findClosestBody( blobs, mouse )]; | |
if( distanceBetween( blob.position, mouse ) < blob.radius + 30 ) { | |
splitBlob( blob ); | |
} | |
} | |
function documentKeyDownHandler(event) { | |
switch( event.keyCode ) { | |
case 40: | |
changeBlobRadius( -10 ); | |
event.preventDefault(); | |
break; | |
case 38: | |
changeBlobRadius( 10 ); | |
event.preventDefault(); | |
break; | |
case 37: | |
changeSkin( -1 ); | |
event.preventDefault(); | |
break; | |
case 39: | |
changeSkin( 1 ); | |
event.preventDefault(); | |
break; | |
} | |
} | |
function keyboardUpHandler(event) { | |
event.preventDefault(); | |
changeBlobRadius( 20 ); | |
} | |
function keyboardDownHandler(event) { | |
event.preventDefault(); | |
changeBlobRadius( -20 ); | |
} | |
function keyboardLeftHandler(event) { | |
event.preventDefault(); | |
changeSkin( -1 ); | |
} | |
function keyboardRightHandler(event) { | |
event.preventDefault(); | |
changeSkin( 1 ); | |
} | |
function changeSkin( offset ) { | |
skinIndex += offset; | |
skinIndex = skinIndex < 0 ? skins.length-1 : skinIndex; | |
skinIndex = skinIndex > skins.length-1 ? 0 : skinIndex; | |
} | |
function changeBlobRadius( offset ) { | |
for( var i = 0, len = blobs.length; i < len; i++ ) { | |
blob = blobs[i]; | |
var oldRadius = blob.radius; | |
blob.radius += offset; | |
blob.radius = Math.max( 40, Math.min( blob.radius, 280 ) ); | |
if( blob.radius != oldRadius ) { | |
blob.updateNormals(); | |
} | |
} | |
} | |
function findClosestBody( bodies, position ) { | |
var closestDistance = 9999; | |
var currentDistance = 9999; | |
var closestIndex = -1; | |
for( var i = 0, len = bodies.length; i < len; i++ ) { | |
var body = bodies[i]; | |
currentDistance = distanceBetween( body.position, { x: position.x, y: position.y } ); | |
if( currentDistance < closestDistance ) { | |
closestDistance = currentDistance; | |
closestIndex = i; | |
} | |
} | |
return closestIndex; | |
} | |
function windowResizeHandler() { | |
SCREEN_WIDTH = window.innerWidth; | |
SCREEN_HEIGHT = window.innerHeight; | |
canvas.width = SCREEN_WIDTH; | |
canvas.height = SCREEN_HEIGHT; | |
worldRect.x = 3; | |
worldRect.y = 3; | |
worldRect.width = SCREEN_WIDTH-6; | |
worldRect.height = SCREEN_HEIGHT-6; | |
} | |
function loop() { | |
var skin = skins[skinIndex]; | |
// The area around the dirty region to include in the clear | |
var dirtySpread = 80; | |
var u1, u2, ulen, blob; | |
// Clear the dirty rects of all blobs | |
for( u1 = 0, ulen = blobs.length; u1 < ulen; u1++ ) { | |
blob = blobs[u1]; | |
// Clear all pixels in the dirty region | |
context.clearRect(blob.dirtyRegion.left-dirtySpread,blob.dirtyRegion.top-dirtySpread,blob.dirtyRegion.right-blob.dirtyRegion.left+(dirtySpread*2),blob.dirtyRegion.bottom-blob.dirtyRegion.top+(dirtySpread*2)); | |
// Reset the dirty region so that it can be expanded anew | |
blob.dirtyRegion = { left: worldRect.x + worldRect.width, top: worldRect.y + worldRect.height, right: 0, bottom: 0 }; | |
} | |
// If there is a merge queued, solve it now | |
if( mergeQueue.blobA != -1 && mergeQueue.blobB != -1 ) { | |
mergeBlobs( mergeQueue.blobA, mergeQueue.blobB ); | |
mergeQueue.blobA = -1; | |
mergeQueue.blobB = -1; | |
} | |
// If the mouse is down, start adding the velocity needed to move towards the mouse position | |
if( dragBlob ) { | |
dragBlob.velocity.x += ( ( mouseX + mouseDownOffset.x ) - dragBlob.position.x ) * 0.01; | |
dragBlob.velocity.y += ( ( mouseY + mouseDownOffset.y ) - dragBlob.position.y ) * 0.01; | |
} | |
for( u1 = 0, ulen = blobs.length; u1 < ulen; u1++ ) { | |
blob = blobs[u1]; | |
for( u2 = 0; u2 < ulen; u2++ ) { | |
var otherBlob = blobs[u2]; | |
if( otherBlob != blob ) { | |
var distance = distanceBetween( { x: blob.position.x, y: blob.position.y }, { x: otherBlob.position.x, y: otherBlob.position.y } ); | |
if( distance < blob.radius + otherBlob.radius ) { | |
mergeQueue.blobA = u1; | |
mergeQueue.blobB = u2; | |
} | |
} | |
} | |
// Track window movement | |
blob.velocity.x += ( window.screenX - screenX ) * (0.04 + (Math.random()*0.1)); | |
blob.velocity.y += ( window.screenY - screenY ) * (0.04 + (Math.random()*0.1)); | |
var friction = { x: 1.035, y: 1.035 }; | |
// Enforce horizontal world bounds | |
if( blob.position.x > worldRect.x + worldRect.width ) { | |
blob.velocity.x -= ( blob.position.x - worldRect.width ) * 0.05; | |
friction.y = 1.07; | |
} | |
else if( blob.position.x < worldRect.x ) { | |
blob.velocity.x += Math.abs( worldRect.x - blob.position.x ) * 0.05; | |
friction.y = 1.07; | |
} | |
// Enforce vertical world bounds | |
if( blob.position.y > worldRect.y + worldRect.height ) { | |
blob.velocity.y -= ( blob.position.y - worldRect.height ) * 0.05; | |
friction.x = 1.07; | |
} | |
else if( blob.position.y < worldRect.y ) { | |
blob.velocity.y += Math.abs( worldRect.y - blob.position.y ) * 0.05; | |
friction.x = 1.07; | |
} | |
// Gravity | |
blob.velocity.x += gravity.x; | |
blob.velocity.y += gravity.y; | |
// Friction | |
blob.velocity.x /= friction.x; | |
blob.velocity.y /= friction.y; | |
// Apply the velocity to the entire blob | |
blob.position.x += blob.velocity.x; | |
blob.position.y += blob.velocity.y; | |
var i, j, len, node, joint, position; | |
// Update all node ghosts (previous positions). All nodes need to be synced before the below | |
// calculation loop to avoid tearing between the first nodes | |
for (i = 0, len = blob.nodes.length; i < len; i++) { | |
node = blob.nodes[i]; | |
node.ghost.x = node.position.x; | |
node.ghost.y = node.position.y; | |
} | |
var dragNode = blob.nodes[blob.dragNodeIndex]; | |
if( dragNode ) { | |
var angle = Math.atan2( mouseY - (blob.position.y-80), mouseX - blob.position.x ); | |
blob.rotation += ( angle - blob.rotation ) * 0.03; | |
blob.updateNormals(); | |
} | |
// Calculation loop | |
for (i = 0, len = blob.nodes.length; i < len; i++) { | |
node = blob.nodes[i]; | |
// Move towards the normal target | |
node.normal.x += ( node.normalTarget.x - node.normal.x ) * 0.05; | |
node.normal.y += ( node.normalTarget.y - node.normal.y ) * 0.05; | |
// This point will be used as the new position for this node, after all factors have been applied | |
position = { x: blob.position.x, y: blob.position.y }; | |
// Apply the joints | |
for( j = 0; j < node.joints.length; j++ ) { | |
joint = node.joints[j]; | |
// Determine the strain on the joints | |
var strainX = ( (joint.node.ghost.x - node.ghost.x) - (joint.node.normal.x - node.normal.x) ); | |
var strainY = ( (joint.node.ghost.y - node.ghost.y) - (joint.node.normal.y - node.normal.y) ); | |
position.x += strainX * joint.strength; | |
position.y += strainY * joint.strength; | |
} | |
// Offset by the normal | |
position.x += node.normal.x; | |
position.y += node.normal.y; | |
// Apply the drag offset (if applicable) | |
if( i == blob.dragNodeIndex ) { | |
position.x += ( mouseX - position.x ) * 0.98; | |
position.y += ( mouseY - position.y ) * 0.98; | |
} | |
// Apply the calculated position to the node (with easing) | |
node.position.x += ( position.x - node.position.x ) * 0.1; | |
node.position.y += ( position.y - node.position.y ) * 0.1; | |
// Limit the node position to screen bounds | |
node.position.x = Math.max( Math.min( node.position.x, worldRect.x + worldRect.width ), worldRect.x ); | |
node.position.y = Math.max( Math.min( node.position.y, worldRect.y + worldRect.height ), worldRect.y ); | |
// Expand the dirty rect if needed | |
blob.dirtyRegion.left = Math.min(blob.dirtyRegion.left, node.position.x); | |
blob.dirtyRegion.top = Math.min(blob.dirtyRegion.top, node.position.y); | |
blob.dirtyRegion.right = Math.max(blob.dirtyRegion.right, node.position.x); | |
blob.dirtyRegion.bottom = Math.max(blob.dirtyRegion.bottom, node.position.y); | |
} | |
if( !skin.debug ) { | |
context.beginPath(); | |
context.fillStyle = skin.fillStyle; | |
context.strokeStyle = skin.strokeStyle; | |
context.lineWidth = skin.lineWidth; | |
} | |
var cn = getArrayElementByOffset( blob.nodes, 0, -1 ); // current node | |
var nn = getArrayElementByOffset( blob.nodes, 0, 0 ); // next node | |
// Move to the first anchor | |
context.moveTo( cn.position.x + ( nn.position.x - cn.position.x ) / 2, cn.position.y + ( nn.position.y - cn.position.y ) / 2 ); | |
// Rendering loop | |
for (i = 0, len = blob.nodes.length; i < len; i++) { | |
cn = getArrayElementByOffset( blob.nodes, i, 0 ); | |
nn = getArrayElementByOffset( blob.nodes, i, 1 ); | |
if( skin.debug ) { | |
context.beginPath(); | |
context.lineWidth = 1; | |
context.strokeStyle = "#ababab"; | |
for( j = 0; j < cn.joints.length; j++ ) { | |
joint = cn.joints[j]; | |
context.moveTo( cn.position.x, cn.position.y ); | |
context.lineTo( joint.node.position.x, joint.node.position.y ); | |
} | |
context.stroke(); | |
context.beginPath(); | |
context.fillStyle = i == 0? "#00ff00" : "#dddddd"; | |
context.arc(cn.position.x, cn.position.y, 5, 0, Math.PI*2, true); | |
context.fill(); | |
} | |
else { | |
context.quadraticCurveTo( cn.position.x, cn.position.y, cn.position.x + ( nn.position.x - cn.position.x ) / 2, cn.position.y + ( nn.position.y - cn.position.y ) / 2 ); | |
} | |
} | |
if( skin.debug ) { | |
// context.beginPath(); | |
// context.fillStyle = "rgba(100,255,100,0.3)"; | |
// context.fillRect(blob.dirtyRegion.left-dirtySpread,blob.dirtyRegion.top-dirtySpread,blob.dirtyRegion.right-blob.dirtyRegion.left+(dirtySpread*2),blob.dirtyRegion.bottom-blob.dirtyRegion.top+(dirtySpread*2)); | |
// context.fill(); | |
} | |
context.stroke(); | |
context.fill(); | |
} | |
screenX = window.screenX; | |
screenY = window.screenY; | |
} | |
}; | |
function Blob() { | |
this.position = { x: 0, y: 0 }; | |
this.velocity = { x: 0, y: 0 }; | |
this.radius = 120; | |
this.quality = 32; | |
this.nodes = []; | |
this.rotation = -Math.PI * 0.5; | |
this.dragNodeIndex = -1; | |
this.dirtyRegion = { left: 0, top: 0, right: 0, bottom: 0 }; | |
this.lastSplitTime = 0; | |
this.generateNodes = function() { | |
this.nodes = []; | |
var i, n; | |
for (i = 0; i < this.quality; i++) { | |
n = { | |
normal: { x: 0, y: 0 }, | |
normalTarget: { x: 0, y: 0 }, | |
position: { x: this.position.x, y: this.position.y }, | |
ghost: { x: this.position.x, y: this.position.y }, | |
angle: 0 | |
}; | |
this.nodes.push( n ); | |
} | |
this.updateJoints(); | |
this.updateNormals(); | |
}; | |
this.updateJoints = function() { | |
for (var i = 0; i < this.quality; i++) { | |
var n = this.nodes[i]; | |
n.joints = [ | |
{ | |
node: getArrayElementByOffset( this.nodes, i, -1 ), | |
strength: 2.2 | |
}, | |
{ | |
node: getArrayElementByOffset( this.nodes, i, 1 ), | |
strength: 2.2 | |
} | |
]; | |
n.joints.push( { | |
node: getArrayElementByOffset( this.nodes, i, -2 ), | |
strength: 2.2 | |
} ); | |
n.joints.push( { | |
node: getArrayElementByOffset( this.nodes, i, 2 ), | |
strength: 2.2 | |
} ); | |
} | |
}; | |
this.updateNormals = function() { | |
var i, j, n; | |
for (i = 0; i < this.quality; i++) { | |
var n = this.nodes[i]; | |
if( this.dragNodeIndex != -1 ) { | |
j = i - this.dragNodeIndex; | |
j = j < 0 ? this.quality + j : j; | |
} | |
else { | |
j = i; | |
} | |
n.angle = ( (j / this.quality ) * Math.PI * 2 ) + this.rotation; | |
n.normalTarget.x = Math.cos( n.angle ) * this.radius; | |
n.normalTarget.y = Math.sin( n.angle ) * this.radius; | |
if( n.normal.x == 0 && n.normal.y == 0 ) { | |
n.normal.x = n.normalTarget.x; | |
n.normal.y = n.normalTarget.y; | |
} | |
} | |
}; | |
this.split = function() { | |
var velocitySpread = this.radius / 10; | |
var nodeSpread = Math.round( this.nodes.length * 0.5 ); | |
var radiusSpread = this.radius * 0.5; | |
var sibling = new Blob(); | |
sibling.position.x = this.position.x; | |
sibling.position.y = this.position.y; | |
sibling.velocity.x = velocitySpread; | |
sibling.velocity.y = this.velocity.y; | |
sibling.nodes = []; | |
var i = 0; | |
while( i++ < nodeSpread ) { | |
sibling.nodes.push( this.nodes.shift() ); | |
} | |
sibling.radius = radiusSpread; | |
sibling.quality = sibling.nodes.length; | |
this.velocity.x = -velocitySpread; | |
this.radius = radiusSpread; | |
this.quality = this.nodes.length; | |
this.dragNodeIndex = -1; | |
this.updateJoints(); | |
this.updateNormals(); | |
sibling.dragNodeIndex = -1; | |
sibling.updateJoints(); | |
sibling.updateNormals(); | |
sibling.lastSplitTime = getTime(); | |
this.lastSplitTime = getTime(); | |
return sibling; | |
}; | |
this.merge = function( sibling ) { | |
this.velocity.x *= 0.5; | |
this.velocity.y *= 0.5; | |
this.velocity.x += sibling.velocity.x * 0.5; | |
this.velocity.y += sibling.velocity.y * 0.5; | |
while( sibling.nodes.length ) { | |
this.nodes.push( sibling.nodes.shift() ); | |
} | |
this.quality = this.nodes.length; | |
this.radius += sibling.radius; | |
this.dragNodeIndex = -1; | |
this.updateNormals(); | |
this.organizeNodesByProximity(); | |
this.updateJoints(); | |
}; | |
this.organizeNodesByProximity = function() { | |
var i, j, outer, inner; | |
var closestDistance, currentDistance, closestIndex; | |
var newNodes = this.nodes.concat(); | |
var blackListed = []; | |
for (i = 0; i < this.quality; i++) { | |
outer = newNodes[i]; | |
currentDistance = 9999; | |
closestDistance = 9999; | |
closestIndex = -1; | |
for(j = 0; j < this.quality; j++) { | |
inner = newNodes[j]; | |
currentDistance = distanceBetween( inner.position, outer.position ); | |
if( currentDistance < closestDistance && blackListed.indexOf(inner) === -1 ) { | |
closestDistance = currentDistance; | |
closestIndex = j; | |
} | |
} | |
this.nodes[i] = newNodes[closestIndex]; | |
} | |
}; | |
} | |
function getArrayElementByOffset( array, index, offset ) { | |
if( array[index+offset] ) { | |
return array[index+offset]; | |
} | |
if( index+offset > array.length-1 ) { | |
return array[index - array.length + offset]; | |
} | |
if( index+offset < 0 ) { | |
return array[array.length + ( index + offset )]; | |
} | |
} | |
function sortByField( list, field ) { | |
var sortOnField = function( a, b ) { | |
return a[field] - b[field]; | |
}; | |
list.sort( sortOnField ); | |
} | |
function getTime() { | |
return new Date().getTime(); | |
} | |
function distanceBetween(p1,p2) { | |
var dx = p2.x-p1.x; | |
var dy = p2.y-p1.y; | |
return Math.sqrt(dx*dx + dy*dy); | |
} | |
BlobWorld.init(); | |
})(); | |
</script> | |
<div id="codepen-footer"> | |
<a style="color: #f73535 !important;" href="https://codepen.wufoo.com/forms/m7x3r3/def/field14=" onclick="window.open(this.href, null, 'height=517, width=680, toolbar=0, location=0, status=1, scrollbars=1, resizable=1'); return false">Report Abuse</a> | |
| |
<a href="/hakimel/pen/blob/1">Edit this Pen</a> | |
</div> | |
</body> | |
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Best seen in fullscreen: http://hakim.se/experiments/html5/blob/03/ | |
*/ | |
BlobWorld = new function() { | |
var SCREEN_WIDTH = window.innerWidth; | |
var SCREEN_HEIGHT = window.innerHeight; | |
var canvas; | |
var context; | |
var blobs = []; | |
var dragBlob; | |
var screenX = window.screenX; | |
var screenY = window.screenY; | |
var mouseX = (window.innerWidth - SCREEN_WIDTH); | |
var mouseY = (window.innerHeight - SCREEN_HEIGHT); | |
var mouseIsDown = false; | |
var mouseDownOffset = { x: 0, y: 0 }; | |
// The bounds of the world | |
var worldRect = { x: 0, y: 0, width: 0, height: 0 }; | |
// The world gravity, applied to all blobs | |
var gravity = { x: 0, y: 1.2 }; | |
// A pair of blobs that should be merged | |
var mergeQueue = { blobA: -1, blobB: -1 }; | |
var skinIndex = 0; | |
var skins = [ | |
{ fillStyle: 'rgba(0,200,250,1.0)', strokeStyle: '#ffffff', lineWidth: 5, debug: false }, | |
{ fillStyle: '', strokeStyle: '', lineWidth: 0, debug: true }, | |
{ fillStyle: 'rgba(0,0,0,0.1)', strokeStyle: 'rgba(255,255,255,1.0)', lineWidth: 6, debug: false }, | |
{ fillStyle: 'rgba(0,230,110,1.0)', strokeStyle: 'rgba(0,0,0,1.0)', lineWidth: 2, debug: false }, | |
{ fillStyle: 'rgba(255,255,0,1.0)', strokeStyle: 'rgba(0,0,0,1.0)', lineWidth: 4, debug: false }, | |
{ fillStyle: 'rgba(255,255,255,1.0)', strokeStyle: 'rgba(0,0,0,1.0)', lineWidth: 4, debug: false } | |
]; | |
this.init = function() { | |
canvas = document.getElementById( 'world' ); | |
if (canvas && canvas.getContext) { | |
context = canvas.getContext('2d'); | |
// Register event listeners | |
document.addEventListener('mousemove', documentMouseMoveHandler, false); | |
canvas.addEventListener('mousedown', documentMouseDownHandler, false); | |
canvas.addEventListener('dblclick', documentDoubleClickHandler, false); | |
document.addEventListener('mouseup', documentMouseUpHandler, false); | |
document.addEventListener('touchstart', documentTouchStartHandler, false); | |
document.addEventListener('touchmove', documentTouchMoveHandler, false); | |
document.addEventListener('touchend', documentTouchEndHandler, false); | |
document.addEventListener('keydown', documentKeyDownHandler, false); | |
window.addEventListener('resize', windowResizeHandler, false); | |
document.getElementById( 'keyboardUp' ).addEventListener('click', keyboardUpHandler, false); | |
document.getElementById( 'keyboardDown' ).addEventListener('click', keyboardDownHandler, false); | |
document.getElementById( 'keyboardLeft' ).addEventListener('click', keyboardLeftHandler, false); | |
document.getElementById( 'keyboardRight' ).addEventListener('click', keyboardRightHandler, false); | |
createBlob( { x: SCREEN_WIDTH*0.5, y: SCREEN_HEIGHT*0.1 } ); | |
windowResizeHandler(); | |
setInterval( loop, 1000 / 60 ); | |
} | |
}; | |
function createBlob( position ) { | |
var blob = new Blob(); | |
blob.position.x = position.x; | |
blob.position.y = position.y; | |
blob.generateNodes(); | |
blobs.push( blob ); | |
} | |
function splitBlob( blob ) { | |
if( blob.quality > 8 ) { | |
blobs.push( blob.split() ); | |
} | |
} | |
function mergeBlobs( blobA, blobB ) { | |
var t = getTime(); | |
if( !blobs[blobA] || !blobs[blobB] ) { | |
return; | |
} | |
if( t - blobs[blobA].lastSplitTime > 500 && t - blobs[blobB].lastSplitTime > 500 ) { | |
// Merge blobB with blobA | |
blobs[blobA].merge( blobs[blobB] ); | |
// Remove blobB since blobA will take over its body | |
blobs.splice( blobB, 1 ); | |
} | |
} | |
function documentMouseMoveHandler(event) { | |
mouseX = event.clientX - (window.innerWidth - SCREEN_WIDTH) * .5; | |
mouseY = event.clientY - (window.innerHeight - SCREEN_HEIGHT) * .5; | |
} | |
function documentMouseDownHandler(event) { | |
event.preventDefault(); | |
mouseIsDown = true; | |
dragBlob = blobs[ findClosestBody( blobs, { x: mouseX, y: mouseY } ) ]; | |
var closestNodeIndex = findClosestBody( dragBlob.nodes, { x: mouseX, y: mouseY } ); | |
dragBlob.dragNodeIndex = closestNodeIndex; | |
mouseDownOffset.y = 100; | |
} | |
function documentMouseUpHandler(event) { | |
mouseIsDown = false; | |
if( dragBlob ) { | |
dragBlob.dragNodeIndex = -1; | |
dragBlob = null; | |
} | |
} | |
function documentTouchStartHandler(event) { | |
if(event.touches.length == 1) { | |
event.preventDefault(); | |
mouseIsDown = true; | |
mouseX = event.touches[0].pageX - (window.innerWidth - SCREEN_WIDTH) * .5; | |
mouseY = event.touches[0].pageY - (window.innerHeight - SCREEN_HEIGHT) * .5; | |
dragBlob = blobs[ findClosestBody( blobs, { x: mouseX, y: mouseY } ) ]; | |
var closestNodeIndex = findClosestBody( dragBlob.nodes, { x: mouseX, y: mouseY } ); | |
dragBlob.dragNodeIndex = closestNodeIndex; | |
mouseDownOffset.y = 100; | |
} | |
} | |
function documentTouchMoveHandler(event) { | |
if(event.touches.length == 1) { | |
event.preventDefault(); | |
mouseX = event.touches[0].pageX - (window.innerWidth - SCREEN_WIDTH) * .5; | |
mouseY = event.touches[0].pageY - (window.innerHeight - SCREEN_HEIGHT) * .5; | |
} | |
} | |
function documentTouchEndHandler(event) { | |
mouseIsDown = false; | |
if( dragBlob ) { | |
dragBlob.dragNodeIndex = -1; | |
dragBlob = null; | |
} | |
} | |
function documentDoubleClickHandler(event) { | |
var mouse = { x: mouseX, y: mouseY }; | |
var blob = blobs[findClosestBody( blobs, mouse )]; | |
if( distanceBetween( blob.position, mouse ) < blob.radius + 30 ) { | |
splitBlob( blob ); | |
} | |
} | |
function documentKeyDownHandler(event) { | |
switch( event.keyCode ) { | |
case 40: | |
changeBlobRadius( -10 ); | |
event.preventDefault(); | |
break; | |
case 38: | |
changeBlobRadius( 10 ); | |
event.preventDefault(); | |
break; | |
case 37: | |
changeSkin( -1 ); | |
event.preventDefault(); | |
break; | |
case 39: | |
changeSkin( 1 ); | |
event.preventDefault(); | |
break; | |
} | |
} | |
function keyboardUpHandler(event) { | |
event.preventDefault(); | |
changeBlobRadius( 20 ); | |
} | |
function keyboardDownHandler(event) { | |
event.preventDefault(); | |
changeBlobRadius( -20 ); | |
} | |
function keyboardLeftHandler(event) { | |
event.preventDefault(); | |
changeSkin( -1 ); | |
} | |
function keyboardRightHandler(event) { | |
event.preventDefault(); | |
changeSkin( 1 ); | |
} | |
function changeSkin( offset ) { | |
skinIndex += offset; | |
skinIndex = skinIndex < 0 ? skins.length-1 : skinIndex; | |
skinIndex = skinIndex > skins.length-1 ? 0 : skinIndex; | |
} | |
function changeBlobRadius( offset ) { | |
for( var i = 0, len = blobs.length; i < len; i++ ) { | |
blob = blobs[i]; | |
var oldRadius = blob.radius; | |
blob.radius += offset; | |
blob.radius = Math.max( 40, Math.min( blob.radius, 280 ) ); | |
if( blob.radius != oldRadius ) { | |
blob.updateNormals(); | |
} | |
} | |
} | |
function findClosestBody( bodies, position ) { | |
var closestDistance = 9999; | |
var currentDistance = 9999; | |
var closestIndex = -1; | |
for( var i = 0, len = bodies.length; i < len; i++ ) { | |
var body = bodies[i]; | |
currentDistance = distanceBetween( body.position, { x: position.x, y: position.y } ); | |
if( currentDistance < closestDistance ) { | |
closestDistance = currentDistance; | |
closestIndex = i; | |
} | |
} | |
return closestIndex; | |
} | |
function windowResizeHandler() { | |
SCREEN_WIDTH = window.innerWidth; | |
SCREEN_HEIGHT = window.innerHeight; | |
canvas.width = SCREEN_WIDTH; | |
canvas.height = SCREEN_HEIGHT; | |
worldRect.x = 3; | |
worldRect.y = 3; | |
worldRect.width = SCREEN_WIDTH-6; | |
worldRect.height = SCREEN_HEIGHT-6; | |
} | |
function loop() { | |
var skin = skins[skinIndex]; | |
// The area around the dirty region to include in the clear | |
var dirtySpread = 80; | |
var u1, u2, ulen, blob; | |
// Clear the dirty rects of all blobs | |
for( u1 = 0, ulen = blobs.length; u1 < ulen; u1++ ) { | |
blob = blobs[u1]; | |
// Clear all pixels in the dirty region | |
context.clearRect(blob.dirtyRegion.left-dirtySpread,blob.dirtyRegion.top-dirtySpread,blob.dirtyRegion.right-blob.dirtyRegion.left+(dirtySpread*2),blob.dirtyRegion.bottom-blob.dirtyRegion.top+(dirtySpread*2)); | |
// Reset the dirty region so that it can be expanded anew | |
blob.dirtyRegion = { left: worldRect.x + worldRect.width, top: worldRect.y + worldRect.height, right: 0, bottom: 0 }; | |
} | |
// If there is a merge queued, solve it now | |
if( mergeQueue.blobA != -1 && mergeQueue.blobB != -1 ) { | |
mergeBlobs( mergeQueue.blobA, mergeQueue.blobB ); | |
mergeQueue.blobA = -1; | |
mergeQueue.blobB = -1; | |
} | |
// If the mouse is down, start adding the velocity needed to move towards the mouse position | |
if( dragBlob ) { | |
dragBlob.velocity.x += ( ( mouseX + mouseDownOffset.x ) - dragBlob.position.x ) * 0.01; | |
dragBlob.velocity.y += ( ( mouseY + mouseDownOffset.y ) - dragBlob.position.y ) * 0.01; | |
} | |
for( u1 = 0, ulen = blobs.length; u1 < ulen; u1++ ) { | |
blob = blobs[u1]; | |
for( u2 = 0; u2 < ulen; u2++ ) { | |
var otherBlob = blobs[u2]; | |
if( otherBlob != blob ) { | |
var distance = distanceBetween( { x: blob.position.x, y: blob.position.y }, { x: otherBlob.position.x, y: otherBlob.position.y } ); | |
if( distance < blob.radius + otherBlob.radius ) { | |
mergeQueue.blobA = u1; | |
mergeQueue.blobB = u2; | |
} | |
} | |
} | |
// Track window movement | |
blob.velocity.x += ( window.screenX - screenX ) * (0.04 + (Math.random()*0.1)); | |
blob.velocity.y += ( window.screenY - screenY ) * (0.04 + (Math.random()*0.1)); | |
var friction = { x: 1.035, y: 1.035 }; | |
// Enforce horizontal world bounds | |
if( blob.position.x > worldRect.x + worldRect.width ) { | |
blob.velocity.x -= ( blob.position.x - worldRect.width ) * 0.05; | |
friction.y = 1.07; | |
} | |
else if( blob.position.x < worldRect.x ) { | |
blob.velocity.x += Math.abs( worldRect.x - blob.position.x ) * 0.05; | |
friction.y = 1.07; | |
} | |
// Enforce vertical world bounds | |
if( blob.position.y > worldRect.y + worldRect.height ) { | |
blob.velocity.y -= ( blob.position.y - worldRect.height ) * 0.05; | |
friction.x = 1.07; | |
} | |
else if( blob.position.y < worldRect.y ) { | |
blob.velocity.y += Math.abs( worldRect.y - blob.position.y ) * 0.05; | |
friction.x = 1.07; | |
} | |
// Gravity | |
blob.velocity.x += gravity.x; | |
blob.velocity.y += gravity.y; | |
// Friction | |
blob.velocity.x /= friction.x; | |
blob.velocity.y /= friction.y; | |
// Apply the velocity to the entire blob | |
blob.position.x += blob.velocity.x; | |
blob.position.y += blob.velocity.y; | |
var i, j, len, node, joint, position; | |
// Update all node ghosts (previous positions). All nodes need to be synced before the below | |
// calculation loop to avoid tearing between the first nodes | |
for (i = 0, len = blob.nodes.length; i < len; i++) { | |
node = blob.nodes[i]; | |
node.ghost.x = node.position.x; | |
node.ghost.y = node.position.y; | |
} | |
var dragNode = blob.nodes[blob.dragNodeIndex]; | |
if( dragNode ) { | |
var angle = Math.atan2( mouseY - (blob.position.y-80), mouseX - blob.position.x ); | |
blob.rotation += ( angle - blob.rotation ) * 0.03; | |
blob.updateNormals(); | |
} | |
// Calculation loop | |
for (i = 0, len = blob.nodes.length; i < len; i++) { | |
node = blob.nodes[i]; | |
// Move towards the normal target | |
node.normal.x += ( node.normalTarget.x - node.normal.x ) * 0.05; | |
node.normal.y += ( node.normalTarget.y - node.normal.y ) * 0.05; | |
// This point will be used as the new position for this node, after all factors have been applied | |
position = { x: blob.position.x, y: blob.position.y }; | |
// Apply the joints | |
for( j = 0; j < node.joints.length; j++ ) { | |
joint = node.joints[j]; | |
// Determine the strain on the joints | |
var strainX = ( (joint.node.ghost.x - node.ghost.x) - (joint.node.normal.x - node.normal.x) ); | |
var strainY = ( (joint.node.ghost.y - node.ghost.y) - (joint.node.normal.y - node.normal.y) ); | |
position.x += strainX * joint.strength; | |
position.y += strainY * joint.strength; | |
} | |
// Offset by the normal | |
position.x += node.normal.x; | |
position.y += node.normal.y; | |
// Apply the drag offset (if applicable) | |
if( i == blob.dragNodeIndex ) { | |
position.x += ( mouseX - position.x ) * 0.98; | |
position.y += ( mouseY - position.y ) * 0.98; | |
} | |
// Apply the calculated position to the node (with easing) | |
node.position.x += ( position.x - node.position.x ) * 0.1; | |
node.position.y += ( position.y - node.position.y ) * 0.1; | |
// Limit the node position to screen bounds | |
node.position.x = Math.max( Math.min( node.position.x, worldRect.x + worldRect.width ), worldRect.x ); | |
node.position.y = Math.max( Math.min( node.position.y, worldRect.y + worldRect.height ), worldRect.y ); | |
// Expand the dirty rect if needed | |
blob.dirtyRegion.left = Math.min(blob.dirtyRegion.left, node.position.x); | |
blob.dirtyRegion.top = Math.min(blob.dirtyRegion.top, node.position.y); | |
blob.dirtyRegion.right = Math.max(blob.dirtyRegion.right, node.position.x); | |
blob.dirtyRegion.bottom = Math.max(blob.dirtyRegion.bottom, node.position.y); | |
} | |
if( !skin.debug ) { | |
context.beginPath(); | |
context.fillStyle = skin.fillStyle; | |
context.strokeStyle = skin.strokeStyle; | |
context.lineWidth = skin.lineWidth; | |
} | |
var cn = getArrayElementByOffset( blob.nodes, 0, -1 ); // current node | |
var nn = getArrayElementByOffset( blob.nodes, 0, 0 ); // next node | |
// Move to the first anchor | |
context.moveTo( cn.position.x + ( nn.position.x - cn.position.x ) / 2, cn.position.y + ( nn.position.y - cn.position.y ) / 2 ); | |
// Rendering loop | |
for (i = 0, len = blob.nodes.length; i < len; i++) { | |
cn = getArrayElementByOffset( blob.nodes, i, 0 ); | |
nn = getArrayElementByOffset( blob.nodes, i, 1 ); | |
if( skin.debug ) { | |
context.beginPath(); | |
context.lineWidth = 1; | |
context.strokeStyle = "#ababab"; | |
for( j = 0; j < cn.joints.length; j++ ) { | |
joint = cn.joints[j]; | |
context.moveTo( cn.position.x, cn.position.y ); | |
context.lineTo( joint.node.position.x, joint.node.position.y ); | |
} | |
context.stroke(); | |
context.beginPath(); | |
context.fillStyle = i == 0? "#00ff00" : "#dddddd"; | |
context.arc(cn.position.x, cn.position.y, 5, 0, Math.PI*2, true); | |
context.fill(); | |
} | |
else { | |
context.quadraticCurveTo( cn.position.x, cn.position.y, cn.position.x + ( nn.position.x - cn.position.x ) / 2, cn.position.y + ( nn.position.y - cn.position.y ) / 2 ); | |
} | |
} | |
if( skin.debug ) { | |
// context.beginPath(); | |
// context.fillStyle = "rgba(100,255,100,0.3)"; | |
// context.fillRect(blob.dirtyRegion.left-dirtySpread,blob.dirtyRegion.top-dirtySpread,blob.dirtyRegion.right-blob.dirtyRegion.left+(dirtySpread*2),blob.dirtyRegion.bottom-blob.dirtyRegion.top+(dirtySpread*2)); | |
// context.fill(); | |
} | |
context.stroke(); | |
context.fill(); | |
} | |
screenX = window.screenX; | |
screenY = window.screenY; | |
} | |
}; | |
function Blob() { | |
this.position = { x: 0, y: 0 }; | |
this.velocity = { x: 0, y: 0 }; | |
this.radius = 120; | |
this.quality = 32; | |
this.nodes = []; | |
this.rotation = -Math.PI * 0.5; | |
this.dragNodeIndex = -1; | |
this.dirtyRegion = { left: 0, top: 0, right: 0, bottom: 0 }; | |
this.lastSplitTime = 0; | |
this.generateNodes = function() { | |
this.nodes = []; | |
var i, n; | |
for (i = 0; i < this.quality; i++) { | |
n = { | |
normal: { x: 0, y: 0 }, | |
normalTarget: { x: 0, y: 0 }, | |
position: { x: this.position.x, y: this.position.y }, | |
ghost: { x: this.position.x, y: this.position.y }, | |
angle: 0 | |
}; | |
this.nodes.push( n ); | |
} | |
this.updateJoints(); | |
this.updateNormals(); | |
}; | |
this.updateJoints = function() { | |
for (var i = 0; i < this.quality; i++) { | |
var n = this.nodes[i]; | |
n.joints = [ | |
{ | |
node: getArrayElementByOffset( this.nodes, i, -1 ), | |
strength: 2.2 | |
}, | |
{ | |
node: getArrayElementByOffset( this.nodes, i, 1 ), | |
strength: 2.2 | |
} | |
]; | |
n.joints.push( { | |
node: getArrayElementByOffset( this.nodes, i, -2 ), | |
strength: 2.2 | |
} ); | |
n.joints.push( { | |
node: getArrayElementByOffset( this.nodes, i, 2 ), | |
strength: 2.2 | |
} ); | |
} | |
}; | |
this.updateNormals = function() { | |
var i, j, n; | |
for (i = 0; i < this.quality; i++) { | |
var n = this.nodes[i]; | |
if( this.dragNodeIndex != -1 ) { | |
j = i - this.dragNodeIndex; | |
j = j < 0 ? this.quality + j : j; | |
} | |
else { | |
j = i; | |
} | |
n.angle = ( (j / this.quality ) * Math.PI * 2 ) + this.rotation; | |
n.normalTarget.x = Math.cos( n.angle ) * this.radius; | |
n.normalTarget.y = Math.sin( n.angle ) * this.radius; | |
if( n.normal.x == 0 && n.normal.y == 0 ) { | |
n.normal.x = n.normalTarget.x; | |
n.normal.y = n.normalTarget.y; | |
} | |
} | |
}; | |
this.split = function() { | |
var velocitySpread = this.radius / 10; | |
var nodeSpread = Math.round( this.nodes.length * 0.5 ); | |
var radiusSpread = this.radius * 0.5; | |
var sibling = new Blob(); | |
sibling.position.x = this.position.x; | |
sibling.position.y = this.position.y; | |
sibling.velocity.x = velocitySpread; | |
sibling.velocity.y = this.velocity.y; | |
sibling.nodes = []; | |
var i = 0; | |
while( i++ < nodeSpread ) { | |
sibling.nodes.push( this.nodes.shift() ); | |
} | |
sibling.radius = radiusSpread; | |
sibling.quality = sibling.nodes.length; | |
this.velocity.x = -velocitySpread; | |
this.radius = radiusSpread; | |
this.quality = this.nodes.length; | |
this.dragNodeIndex = -1; | |
this.updateJoints(); | |
this.updateNormals(); | |
sibling.dragNodeIndex = -1; | |
sibling.updateJoints(); | |
sibling.updateNormals(); | |
sibling.lastSplitTime = getTime(); | |
this.lastSplitTime = getTime(); | |
return sibling; | |
}; | |
this.merge = function( sibling ) { | |
this.velocity.x *= 0.5; | |
this.velocity.y *= 0.5; | |
this.velocity.x += sibling.velocity.x * 0.5; | |
this.velocity.y += sibling.velocity.y * 0.5; | |
while( sibling.nodes.length ) { | |
this.nodes.push( sibling.nodes.shift() ); | |
} | |
this.quality = this.nodes.length; | |
this.radius += sibling.radius; | |
this.dragNodeIndex = -1; | |
this.updateNormals(); | |
this.organizeNodesByProximity(); | |
this.updateJoints(); | |
}; | |
this.organizeNodesByProximity = function() { | |
var i, j, outer, inner; | |
var closestDistance, currentDistance, closestIndex; | |
var newNodes = this.nodes.concat(); | |
var blackListed = []; | |
for (i = 0; i < this.quality; i++) { | |
outer = newNodes[i]; | |
currentDistance = 9999; | |
closestDistance = 9999; | |
closestIndex = -1; | |
for(j = 0; j < this.quality; j++) { | |
inner = newNodes[j]; | |
currentDistance = distanceBetween( inner.position, outer.position ); | |
if( currentDistance < closestDistance && blackListed.indexOf(inner) === -1 ) { | |
closestDistance = currentDistance; | |
closestIndex = j; | |
} | |
} | |
this.nodes[i] = newNodes[closestIndex]; | |
} | |
}; | |
} | |
function getArrayElementByOffset( array, index, offset ) { | |
if( array[index+offset] ) { | |
return array[index+offset]; | |
} | |
if( index+offset > array.length-1 ) { | |
return array[index - array.length + offset]; | |
} | |
if( index+offset < 0 ) { | |
return array[array.length + ( index + offset )]; | |
} | |
} | |
function sortByField( list, field ) { | |
var sortOnField = function( a, b ) { | |
return a[field] - b[field]; | |
}; | |
list.sort( sortOnField ); | |
} | |
function getTime() { | |
return new Date().getTime(); | |
} | |
function distanceBetween(p1,p2) { | |
var dx = p2.x-p1.x; | |
var dy = p2.y-p1.y; | |
return Math.sqrt(dx*dx + dy*dy); | |
} | |
BlobWorld.init(); | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<p>Double click to split. <a id="keyboardUp" href="#">Increase</a> / <a id="keyboardDown" href="#">decrease</a> size or <a id="keyboardLeft" href="#">Previous</a> / <a id="keyboardRight" href="#">Next</a> skin.</p> | |
<canvas id='world'></canvas> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
body { | |
background-color: #333333; | |
padding: 0; | |
margin: 0; | |
overflow: hidden; | |
} | |
p { | |
position: absolute; | |
z-index: 99; | |
color: #cccccc; | |
margin: 2px; | |
padding: 0; | |
font-family: Arial; | |
font-size: 10px; | |
} | |
a { | |
color: #f4f4f4; | |
font-weight: bold; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment