Skip to content

Instantly share code, notes, and snippets.

@monfera
Last active September 15, 2016 13:40
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save monfera/64ae67f49015960b99983d9fecf6db59 to your computer and use it in GitHub Desktop.
Save monfera/64ae67f49015960b99983d9fecf6db59 to your computer and use it in GitHub Desktop.
Animal-like random force network
license: gpl-3.0

Animal-like behavior generated by a force network (Verlet). Random motion in a constrained system results in scouting locomotion.

Unless alphaDecay is set to 0, the animal goes through some metamorphosis and gets fossilized at the end.

Built with blockbuilder.org

<!DOCTYPE html>
<head>
<meta charset="utf-8">
<script src="https://d3js.org/d3.v4.min.js"></script>
<style>
body { margin:0;position:fixed;top:0;right:0;bottom:0;left:0; }
</style>
</head>
<body>
<canvas id="main" width="960" height="500"></canvas>
<script>
const mainCanvasWidth = 960
const mainCanvasHeight = 500
const segmentCount = 24
const baseStrength = 0.5
const canvas = document.querySelector('canvas#main')
const ctx = canvas.getContext('2d', {alpha: false})
ctx.transform(1, 0, 0, 1, mainCanvasWidth / 2, mainCanvasHeight / 2)
const id = d => d.id
const radius = node => node.radius
const distance = link => link.distance
const thickness = segmentNumber => 1 + 8 * Math.sin(Math.PI * (0.15 + 0.85 * segmentNumber / segmentCount))
const segment = segmentNumber => {
const distance = thickness(segmentNumber)
const radius = distance * 3/4
return {
number: segmentNumber,
nodes: [
{ radius, id: `segment ${segmentNumber} front dorsal`, x: -20 * (segmentNumber - 1) + 200, y: 16, skin: true },
{ radius, id: `segment ${segmentNumber} front median`, x: -20 * (segmentNumber - 1) + 200, y: 0, skin: true },
{ radius, id: `segment ${segmentNumber} front ventral`, x: -20 * (segmentNumber - 1) + 200, y: -16, skin: true },
{ radius, id: `segment ${segmentNumber} back dorsal`, x: -20 * segmentNumber + 200, y: 8 },
{ radius, id: `segment ${segmentNumber} back ventral`, x: -20 * segmentNumber + 200, y: -8 }
],
links: [
{ distance, strength: 0, source: `segment ${segmentNumber} front dorsal`, target: `segment ${segmentNumber} front ventral`, skin: true },
{ distance, strength: baseStrength, source: `segment ${segmentNumber} front dorsal`, target: `segment ${segmentNumber} front median`, skin: true },
{ distance, strength: baseStrength, source: `segment ${segmentNumber} front median`, target: `segment ${segmentNumber} front ventral`, skin: true },
{ distance, strength: baseStrength, source: `segment ${segmentNumber} back dorsal`, target: `segment ${segmentNumber} back ventral` },
{ distance, strength: baseStrength, source: `segment ${segmentNumber} front dorsal`, target: `segment ${segmentNumber} back dorsal` },
{ distance, strength: baseStrength, source: `segment ${segmentNumber} front median`, target: `segment ${segmentNumber} back dorsal` },
{ distance, strength: baseStrength, source: `segment ${segmentNumber} front median`, target: `segment ${segmentNumber} back ventral` },
{ distance, strength: baseStrength, source: `segment ${segmentNumber} front ventral`, target: `segment ${segmentNumber} back ventral` }
]
}
}
const bodyNodes = d3.range(0, segmentCount).map(segment)
const body = bodyNodes.reduce((prev, next) => {
const prevsegmentNodes = prev.nodes.length > 0 && prev.nodes.slice(-2)
const nodes = prev.nodes.concat(next.nodes)
const segmentNumber = next.number
const distance = thickness(segmentNumber)
let links = prev.links.concat(next.links)
if(prevsegmentNodes) {
links = links.concat([
{distance, strength: baseStrength, source: `segment ${segmentNumber - 1} back dorsal`, target: `segment ${segmentNumber} front dorsal`},
{distance, strength: baseStrength, source: `segment ${segmentNumber - 1} back ventral`, target: `segment ${segmentNumber} front ventral`},
{distance, strength: baseStrength, source: `segment ${segmentNumber - 1} back dorsal`, target: `segment ${segmentNumber} front median`},
{distance, strength: baseStrength, source: `segment ${segmentNumber - 1} back ventral`, target: `segment ${segmentNumber} front median`},
{distance, strength: -0.1 * baseStrength, source: `segment ${segmentNumber - 1} front dorsal`, target: `segment ${segmentNumber} front dorsal`},
{distance, strength: -0.1 * baseStrength, source: `segment ${segmentNumber - 1} front median`, target: `segment ${segmentNumber} front median`},
{distance, strength: -0.1 * baseStrength, source: `segment ${segmentNumber - 1} front ventral`, target: `segment ${segmentNumber} front ventral`}
])
}
return { nodes, links }
}, {nodes: [], links: []})
const {nodes, links} = body
ctx.clearRect(-mainCanvasWidth / 2, - mainCanvasHeight / 2, mainCanvasWidth, mainCanvasHeight)
const strength = link => link.strength
let speedOfChange = 1
const randomSpeedChange = alpha => {
let node
let i
for (i = 0; i < nodes.length; i++) {
node = nodes[i]
if(node.skin) continue
node.vx += 2 * (Math.random() - 0.4) * speedOfChange
node.vy += 2 * (Math.random() - 0.4) * speedOfChange
}
speedOfChange = Math.min(5, Math.max(-5, speedOfChange + Math.random() - 0.5))
}
const fadeOut = () => {
ctx.fillStyle = "rgba(255, 255, 255, 0.005)"
ctx.fillRect(-mainCanvasWidth / 2, - mainCanvasHeight / 2, mainCanvasWidth, mainCanvasHeight)
requestAnimationFrame(fadeOut)
}
d3.forceSimulation(nodes)
.alphaDecay(0.001) // set it to zero for the worm to live forever
.alphaTarget(0)
.velocityDecay(0.1)
.force("repulsion", d3.forceManyBody().strength(-0.1).distanceMin(0).distanceMax(100))
.force("center", d3.forceCenter())
.force("link", d3.forceLink(links).id(id).strength(strength).distance(distance).iterations(10))
.force("collide", d3.forceCollide().radius(radius))
.force("randomSpeedChange", randomSpeedChange)
.on("tick", draw)
.on("end", fadeOut)
function draw() {
ctx.fillStyle = "rgba(0, 0, 0, 0.4)"
ctx.fillRect(-mainCanvasWidth / 2, - mainCanvasHeight / 2, mainCanvasWidth, mainCanvasHeight)
ctx.beginPath()
ctx.strokeStyle = "rgba(255, 255, 255, 0.5)"
ctx.lineWidth = 1
links.filter(d => !d.skin).forEach(drawLink)
ctx.stroke()
ctx.beginPath()
ctx.strokeStyle = "rgba(255, 255, 255, 0.3)"
ctx.lineWidth = 15
links.filter(d => d.skin).forEach(drawLink)
ctx.stroke()
ctx.beginPath()
ctx.lineWidth = 1
nodes.forEach(drawNode)
ctx.fill()
ctx.fillStyle = "rgba(255, 255, 255, 0.4)"
ctx.strokeStyle = "rgba(255, 255, 255, 0.4)"
ctx.stroke()
}
const drawLink = d => {
ctx.moveTo(d.source.x, d.source.y)
ctx.lineTo(d.target.x, d.target.y)
}
const drawNode = d => {
const r = d.skin ? d.radius : 0.5
ctx.moveTo(d.x + r, d.y)
ctx.arc(d.x, d.y, r, 0, 2 * Math.PI)
}
</script>
</body>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment