Skip to content

Instantly share code, notes, and snippets.

@monfera
Last active March 5, 2018 10:38
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save monfera/2d2809d8458ffb81cc9acab2e65ed4ef to your computer and use it in GitHub Desktop.
Save monfera/2d2809d8458ffb81cc9acab2e65ed4ef to your computer and use it in GitHub Desktop.
Mixing drinks on Tralfamadore
license: none
scrolling: no
border: yes

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

<!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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment