block by monfera 2d2809d8458ffb81cc9acab2e65ed4ef

Mixing drinks on Tralfamadore

Full Screen

Evokes memories of interstellar Summer trips. Wait a bit for some color and shake changes. Refresh the page for different liquid volumes, color palettes and color change rates.

It can take a while to get a homogenous solution even with swift particles and shaking. If it’s janky inside the iframe, the Open link may work better.

It’s a simple application of the versatile Verlet integration built into D3 4.0, which calls the fast, non-recursive quadtree implementation for collision resolution and many-body forces - the latter of which was temporarily tried for simulating H-bonds, it would add a bit of surface tension.

Almost all of the new, sequential color scales show up.

Tralfamadore is a fictional planet in several fantastic books by Kurt Vonnegut.

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;overflow:hidden }
  </style>
</head>

<body>
  <canvas width="960" height="500" style="background-color: black"></canvas>
  <script type="text/javascript">

var width = 960
var height = 500

var particleCount = 1500

// some of the settings change on every reload
var particleRadius = 4 + Math.random() * 6
var paintSizeRatio = 0.8 + 0.4 * Math.random()
var sideWallRadius = height / 3 // confining balls on the sides
var planetRadius = 100000 // 'planet' underneath, almost no curvature
var recycle = true // lost liquid rains back
var amplCycle = 30000 // 30s

// utilities
var radius = function(element) { return element.r }
var startMs = Date.now()
var getMs = performance && performance.now
            ? function() { return performance.now() }
            : function() { return Date.now() - startMs }
var TAU = 2 * Math.PI

// please turn off your mobile phones
if(window.parent && window.parent.document) {
  window.parent.document.body.style.backgroundColor = "black"
  window.parent.document.body.style.color = "grey"
}

// initial render setup
var ctx = document.querySelector('canvas').getContext('2d')
ctx.transform(1, 0, 0, -1, width / 2, height / 2) // I ❤ WebGL-like projection
ctx.fillStyle = "rgba(0,0,0,1)"
ctx.rect(-width / 2, - height / 2, width, height)
ctx.fill()
ctx.lineWidth = particleRadius / 2 * paintSizeRatio
ctx.fillStyle = "rgba(0,0,0,0.05)"

// particle data
var particles = d3.range(particleCount).map(function(d, i) {
  return {
    x: (i < particleCount >> 1 ? -1 : 1) * (Math.random() * width / 2 ) / 2,
    y:  particleRadius / 30 * Math.random() * height - height / 2 + 40,
    r: particleRadius
  }
})

// liquid container walls - one circle per side and one 'planet' underneath
var walls = [
  {r: sideWallRadius},
  {r: sideWallRadius},
  {r: planetRadius}
]

// this is an imperfect way of constraining the container walls
var lockWallsInPlace = function() {
  var t = getMs()
  var amplitude = 0.75 + 0.25 * Math.sin((t % amplCycle) / amplCycle * TAU)
  walls[0].x = - width / 2 + sideWallRadius / 3
               * Math.pow(amplitude, 3) * Math.cos(t / TAU / 100)
  walls[0].y = - height / 4
  walls[1].x = width / 2  + sideWallRadius / 3
               * Math.pow(amplitude, 3) * Math.cos(t / TAU / 100)
  walls[1].y = - height / 4
  walls[2].x = 0
  walls[2].y = - height / 2 - planetRadius + 40 // let's see some of it
  walls[0].vx = walls[0].vy = 0
  walls[1].vx = walls[1].vy = 0
  walls[2].vx = walls[2].vy = 0
}
lockWallsInPlace()

// gravity-like force ... with rain cycle, if needed
function gravity() {
  var p
  for (var i = 0; i < particles.length; i++) {
    p = particles[i]
    p.vy -= Math.min(0.5, Math.max(0, (p.y - (- height / 2 + 40)) / height ))
    if(recycle && p.y < - height / 2) {
      p.x = 2 * width * (Math.random() - 0.5) // double wide area for slow rain
      p.vx = Math.random() - 0.5
      p.vy = -10
      p.y = height / 2
    }
  }
}

// simulation setup
d3.forceSimulation(walls.concat(particles))
  .alphaDecay(0)
  .velocityDecay(0)
  .force("gravity", gravity)
  .force("collide", d3.forceCollide().radius(radius).iterations(1)
    .strength(0.05 + Math.random() * 0.25))
  .force("lockInPlace", lockWallsInPlace)
  .on("tick", render)

// coloring setup
var cycleLen1 = 5000 + Math.random() * 55000
var cycleLen2 = 5000 + Math.random() * 55000

var palettes = [
  d3.interpolateViridis,
  d3.interpolateMagma,
  d3.interpolatePlasma,
  d3.interpolateWarm,
  d3.interpolateCool,
  d3.interpolateRainbow,
  d3.interpolateCubehelixDefault
]

// pick from the new continuous color palettes
var palette1 = palettes[Math.floor(palettes.length * Math.random())]
var palette2 = palettes[Math.floor(palettes.length * Math.random())]

// canvas render - plenty fast for this, no need for WebGL
// palettes are traversed both ways to avoid disruption
function render() {

  var i
  var particle
  var t = getMs()

  // vary how much trail the particles leave
  ctx.beginPath()
  ctx.fillStyle = "rgba(0,0,0," + Math.pow((0.25 + 0.75
                  * (1 + Math.sin(t / cycleLen1 * TAU)) / 2), 2)  + ")"
  ctx.rect(-width / 2, - height / 2, width, height)
  ctx.fill()

  // draw one half of the particles with a color
  ctx.strokeStyle = palette1(Math.abs(2 * (t % cycleLen1) / cycleLen1 - 1))
  ctx.beginPath()
  for(i = 0; i < (particleCount >> 1); i++) {
    particle = particles[i]
    ctx.moveTo(particle.x - particle.r * .25 * paintSizeRatio, particle.y)
    ctx.lineTo(particle.x + particle.r * .25 * paintSizeRatio, particle.y)
  }
  ctx.stroke()

  // draw the other half of the particles with another color
  ctx.strokeStyle = palette2(Math.abs(2 * (t % cycleLen2) / cycleLen2 - 1))
  ctx.beginPath()
  for(i = (particleCount >> 1); i < particleCount; i++) {
    particle = particles[i]
    ctx.moveTo(particle.x - particle.r * .25 * paintSizeRatio, particle.y)
    ctx.lineTo(particle.x + particle.r * .25 * paintSizeRatio, particle.y)
  }
  ctx.stroke()

  // draw the side circles, making it look a bit reflective
  ctx.beginPath()
  ctx.fillStyle = "rgba(0,0,0,0.5)"
  for(i = 0; i < 2; i++) {
    particle = walls[i]
    ctx.moveTo(particle.x + particle.r, particle.y)
    ctx.arc(particle.x, particle.y, particle.r, 0, TAU)
  }
  ctx.fill()

/*
  // this block, if switched on, hides the fact that the nodes can overlap,
  // here with the planet-like container bottom circle, because
  // the collision iterator count is kept minimal due to the need for speed
  context.beginPath()
  context.fillStyle = "rgba(0,0,0,1)"
  for(i = 2; i < 3; i++) {
    particle = walls[i]
    context.moveTo(particle.x + particle.r, particle.y)
    context.arc(particle.x, particle.y, particle.r, 0, tau)
  }
  context.fill()
*/
}    
    
  </script>
</body>