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 i
th 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).
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):
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.
<!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>