block by tophtucker 500d2a010105cfcc87db

K-Hole

Full Screen

Factoring out the geometric background part of the opener for this Bloomberg Businessweek story on ketamine. Move your mouse left and right to change the speed. Move your mouse up and down to change the amplitude.

Text can be overlaid with the query string variable text, e.g. http://bl.ocks.org/tophtucker/raw/500d2a010105cfcc87db/?text=KETAMINE.

It’s a bunch of concentric circles with exclusion compositing. The radius of the ith circle oscillates with amplitude * Math.sin(t/speed + (t/10000)*i), where t is the number of milliseconds since you loaded the page.

E.g., for amplitude = 5 and speed = 500 (which is the default if you position your mouse in the middle of the page), the radius of circle i at time t oscillates with period (20000π)/(20+i) in t (WolframAlpha).

2D plot of radius with respect to t and i 3D plot of radius with respect to t and i

Things sync up in cool ways when the derivative with respect to i (which comes out to (t cos(((i+20) t)/10000))/2000) is small (WolframAlpha):

Derivative of radius with respect to i

Um because then the concentric circles oscillate in phase? Or close to it? Yeah… yeah that sounds right… right?

Obviously I just mashed periodic functions and variables and parameters together in various terms of various order until some li’l unexpected emergent thing happened.

index.html

<!DOCTYPE html>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">

<style>
canvas {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  overflow: hidden;
  cursor: none;
}
</style>

<canvas></canvas>

<script src="d3.min.js" charset="utf-8"></script>
<script>

var canvas = d3.select("canvas"),
    w,
    h,
    mouse = [0,0],
    mouseTimeout,
    circles,
    numberOfCircles = 50,
    bandSize = 10,
    amplitude = 5,
    speed = 500,
    amplitudeScale = d3.scale.linear(),
    speedScale = d3.scale.linear(),
    screenScale = d3.scale.linear()
      .domain([320,1280,2560])
      .range([.5,1,2])
    textSizeScale = d3.scale.linear()
      .domain([1280, 320])
      .range([180, 60]);


// set up canvas
var ctx = canvas.node().getContext("2d");
ctx.fillStyle = "#000000";
ctx.globalCompositeOperation = "xor";
handleSizing();

// bindings
d3.select(window).on("resize", handleSizing);
canvas.on("mousemove", handleMouse);
canvas.on("touchmove", handleMouse);

d3.timer(render);

function render(t) {

  // clear the canvas
  ctx.clearRect(0,0,w,h);

  // draw the background grid
  d3.range(20).map(function(i) {
    var baseBandWidth = w / 39;
    var offset = 2*baseBandWidth * i;
    var bandWidth = baseBandWidth * (Math.abs(offset - mouse[0])/w + 0.5);
    ctx.fillRect(offset, 0, bandWidth, h);
  })
  d3.range(20).map(function(i) {
    var baseBandWidth = h / 39;
    var offset = 2*baseBandWidth * i;
    var bandWidth = baseBandWidth * (Math.abs(offset - mouse[1])/h + 0.5);
    ctx.fillRect(0, offset, w, bandWidth);
  })

  // draw the circles
  ctx.globalCompositeOperation = "source-over";
  circles = d3.range(numberOfCircles).map(function(i) { 
    var baseBandSize = screenScale(w) * bandSize * (numberOfCircles - i)
    var oscillation = screenScale(w) * amplitude * Math.sin(t/speed + (t/10000)*i); 
    var radius = Math.max(0, baseBandSize + oscillation);
    return radius;
  }).sort(d3.descending);  
  circles.forEach(function(d, i) {
    ctx.beginPath();
    ctx.arc(w/2, h/2, d, 0, 2 * Math.PI, false);
    ctx.fill();
    ctx.globalCompositeOperation = "xor";
  })

  // draw text
  if(getQueryVariable("text")) {
    ctx.fillText(getQueryVariable("text"), w/2, h/2);
  }

}

function handleSizing() {
  speedScale
    .domain([0,innerWidth/2,innerWidth])
    .range([2000,500,50]);

  amplitudeScale
    .domain([0,innerHeight/2,innerHeight])
    .range([1,5,50]);

  canvas
    .attr("width", innerWidth)
    .attr("height", innerHeight);

  w = canvas.node().width;
  h = canvas.node().height;

  ctx.font = "bold "+textSizeScale(w)+"px 'Helvetica'";
  ctx.textBaseline = 'middle';
  ctx.textAlign = "center";
}

function handleMouse() {
  mouse = d3.touch(this) || d3.mouse(this);

  // re-parameterize
  speed = speedScale(mouse[0]);
  amplitude = amplitudeScale(mouse[1]);

  // show mouse for 500ms
  canvas.style("cursor", "crosshair")
  clearTimeout(mouseTimeout);
  mouseTimeout = setTimeout(function() {
    canvas.style("cursor", "none");
  },500);
}

// https://css-tricks.com/snippets/javascript/get-url-variables/
function getQueryVariable(variable) {
  var query = window.location.search.substring(1);
  var vars = query.split("&");
  for (var i=0;i<vars.length;i++) {
    var pair = vars[i].split("=");
    if(pair[0] == variable){return pair[1];}
  }
  return(false);
}

</script>