Pen Settings

HTML

CSS

CSS Base

Vendor Prefixing

Add External Stylesheets/Pens

Any URL's added here will be added as <link>s in order, and before the CSS in the editor. If you link to another Pen, it will include the CSS from that Pen. If the preprocessor matches, it will attempt to combine them before processing.

+ add another resource

JavaScript

Babel includes JSX processing.

Add External Scripts/Pens

Any URL's added here will be added as <script>s in order, and run before the JavaScript in the editor. You can use the URL of any other Pen and it will include the JavaScript from that Pen.

+ add another resource

Packages

Add Packages

Search for and use JavaScript packages from npm here. By selecting a package, an import statement will be added to the top of the JavaScript editor for this package.

Behavior

Save Automatically?

If active, Pens will autosave every 30 seconds after being saved once.

Auto-Updating Preview

If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.

Format on Save

If enabled, your code will be formatted when you actively save your Pen. Note: your code becomes un-folded during formatting.

Editor Settings

Code Indentation

Want to change your Syntax Highlighting theme, Fonts and more?

Visit your global Editor Settings.

HTML

              
                <canvas>
              
            
!

CSS

              
                body {
  margin: 0;
  overflow: hidden;
}
              
            
!

JS

              
                const STG = {
  sensorsOffset: 80,
  nbSensors: 8,
  noiseScale: 0.003,
  noiseOffset: 0,
  noiseExponent: 1.4,
  invFriction: 0.98,
  attrStrength: 1,
}

const gui = new dat.GUI()
gui.add(STG, "sensorsOffset", 0, 200)
const offsetCtrl = gui.add(STG, "noiseOffset", 0, 3)
const nsExponentCtrl = gui.add(STG, "noiseExponent", 0.1, 4)
gui.add(STG, "invFriction", 0.9, 0.9999)
gui.add(STG, "attrStrength", 0.5, 4)

const cvs = document.querySelector("canvas")
const ctx = cvs.getContext("2d")
let W, H
cvs.width = W = window.innerWidth
cvs.height = H = window.innerHeight


// generate a noise map
const simplex = new SimplexNoise("aaa")
const noiseMap = new Float32Array(W*H)
const noiseMapUint8 = new Uint8ClampedArray(W*H*4)
const index = (x, y) => x + y*W
// the ImageData which will store the noise to be drawn
let noiseMapImg
// fills the noise bases on the
const updateNoise = () => {
  for (let x = 0; x < W; x++) {
    for (let y = 0; y < H; y++) {
      const idx = index(x, y)
      let N = simplex.noise3D(x * STG.noiseScale, y * STG.noiseScale, STG.noiseOffset) * .5 + .5
      N = Math.pow(N, STG.noiseExponent)
      const Nint = (N*255)|0
      noiseMap[idx] = N
      noiseMapUint8[idx*4] = Nint
      noiseMapUint8[idx*4+1] = Nint
      noiseMapUint8[idx*4+2] = Nint
      noiseMapUint8[idx*4+3] = 255
    }
  }
  // put the noise map data into ImageData for quicker rendering
  noiseMapImg = new ImageData(noiseMapUint8, W, H)
}
offsetCtrl.onChange(updateNoise)
nsExponentCtrl.onChange(updateNoise)
updateNoise()


// initialize the particle & sensors
const particle = {
  x: Math.random()*W,
  y: Math.random()*H,
  vx: 0,
  vy: 0,
  ax: 0,
  ay: 0
}
const sensors = new Array(STG.nbSensors)
for (let i = 0; i < sensors.length; i++) {
  sensors[i] = {
    angle: i/sensors.length * 2*Math.PI,
    weight: 0,
    x: 0,
    y: 0,
    fakeX: 0,
    fakeY: 0
  }
}

// add a button to reset the particle
const btnCtrl = {
  resetParticle: () => {
    particle.x = Math.random()*W
    particle.y = Math.random()*H
    particle.vx = 0
    particle.vy = 0
    particle.ax = 0
    particle.ay = 0
  }
}
gui.add(btnCtrl, "resetParticle")


// toroidal topology (coordinates that loop on the edges)
const toroidal = (x, y) => {
  if (x > W) x%= W
  else if (x < 0) x = W + x
  if (y > H) y%= H
  else if (y < 0) y = H + y
  return { x, y }
}


const update = () => {
  // update each sensor by sampling the noise map
  for (const sensor of sensors) {
    // get sensor position
    sensor.fakeX = particle.x + Math.cos(sensor.angle) * STG.sensorsOffset
    sensor.fakeY = particle.y + Math.sin(sensor.angle) * STG.sensorsOffset
    const Spos = toroidal(sensor.fakeX, sensor.fakeY)
    const idx = index(Spos.x|0, Spos.y|0)
    sensor.x = Spos.x
    sensor.y = Spos.y
    sensor.weight = noiseMap[idx]
  }
  
  // compute the direction based on the weight of each sensor
  let dirX = 0
  let dirY = 0
  for (const sensor of sensors) {
    dirX+= Math.cos(sensor.angle) * sensor.weight
    dirY+= Math.sin(sensor.angle) * sensor.weight
  }
  dirX/= STG.nbSensors
  dirY/= STG.nbSensors
  particle.ax = dirX
  particle.ay = dirY
  
  // now update the particle
  particle.vx+= particle.ax * STG.attrStrength
  particle.vy+= particle.ay * STG.attrStrength
  particle.x+= particle.vx
  particle.y+= particle.vy
  particle.vx*= STG.invFriction
  particle.vy*= STG.invFriction
  
  // toroidal topology
  const { x, y } = toroidal(particle.x, particle.y)
  particle.x = x
  particle.y = y
}


const drawVector = (cx, cy, x, y) => {
  ctx.save()
  ctx.translate(cx, cy)
  const angle = Math.atan2(y, x)
  const len = Math.sqrt(x**2 + y**2)
  ctx.rotate(angle)
  // the main line
  ctx.beginPath()
  ctx.moveTo(0, 0)
  ctx.translate(len, 0)
  ctx.lineTo(0, 0)
  ctx.stroke()
  // the arrow
  ctx.beginPath()
  ctx.moveTo(-5, 5)
  ctx.lineTo(0, 0)
  ctx.lineTo(-5, -5)
  ctx.stroke()
  ctx.restore()
}


const draw = () => {
  // clear the background
  ctx.fillStyle = "black"
  ctx.fillRect(0, 0, W, H)
  
  // draw the simplex noise
  ctx.putImageData(noiseMapImg, 0, 0)
  
  // then the sensors
  for (const sensor of sensors) {
    // draw the link
    ctx.strokeStyle = "blue"
    ctx.lineWidth = 1.5
    ctx.beginPath()
    ctx.moveTo(particle.x, particle.y)
    ctx.lineTo(sensor.fakeX, sensor.fakeY)
    ctx.stroke()
    
    // draw the sensor
    ctx.lineWidth = 2
    const val = (sensor.weight*255)|0
    ctx.fillStyle = `rgb(${val}, ${val}, ${val})`
    ctx.beginPath()
    ctx.arc(sensor.x, sensor.y, 8, 0, 2*Math.PI)
    ctx.fill()
    ctx.stroke()
    
    // draw the sensor weight, so that it's always visible
    ctx.strokeStyle = "#00ff00"
    drawVector(
      particle.x, 
      particle.y, 
      Math.cos(sensor.angle) * sensor.weight * STG.sensorsOffset, 
      Math.sin(sensor.angle) * sensor.weight * STG.sensorsOffset
    )
  }
  
  // draw the particle
  ctx.fillStyle = "white"
  ctx.beginPath()
  ctx.arc(particle.x, particle.y, 18, 0, 2*Math.PI)
  ctx.fill()
  ctx.fillStyle = "red"
  ctx.beginPath()
  ctx.arc(particle.x, particle.y, 16, 0, 2*Math.PI)
  ctx.fill()
  
  // draw the direction of the particle
  ctx.strokeStyle = "red"
  ctx.lineWidth = 4
  const mag = Math.sqrt(particle.ax**2 + particle.ay**2)
  const dx = particle.ax / mag
  const dy = particle.ay / mag
  const L = Math.min(300, Math.max(30, mag * 500))
  
  drawVector(particle.x, particle.y, dx * L, dy * L)
}


// main loop
const loop = () => {
  requestAnimationFrame(loop)
  update()
  draw()
}

loop()
              
            
!
999px

Console