block by monfera 64ae67f49015960b99983d9fecf6db59

Animal-like random force network

Full Screen

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

index.html

<!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>