Skip to content

Instantly share code, notes, and snippets.

@tophtucker
Last active September 13, 2019 10:09
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tophtucker/6d838d47e1721b2437b94f36865f56cd to your computer and use it in GitHub Desktop.
Save tophtucker/6d838d47e1721b2437b94f36865f56cd to your computer and use it in GitHub Desktop.
Drifting flyers on a torus

Alternate illustrative concept for Businessweek’s Max Chafkin & Brian Womack’s April 2016 feature on Yahoo. Move the mouse to fling the flyers around. Flyers are accelerated in the direction the mouse is moving, proportionate to their proximity. Then there’s air friction and gravity, so the flyers resume an equilibrium downward path along the torus.

When I say “torus” I just mean that it wraps around at the top/bottom and left/right edges, which topologically is a torus, right? But I dropped topology class after like a week. Also, gravity’s a vector, so easily configurable!!!!! I should really use a little vector math library. I-should-buy-a-boat.jpg.

Flyer (& whole cover & feature design) by Tracy Ma.

To-do:

  • Touch-and-drag support
  • Do something with little torn-off bits of flyer?
<!DOCTYPE html>
<html>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title></title>
<style>
* {
box-sizing: border-box;
}
html, body {
margin: 0;
padding: 0;
overflow-x: hidden;
font-family: sans-serif;
}
.opener {
position: fixed;
z-index: 1;
padding: 5em;
width: calc(100% + 2 * 133px);
height: calc(100% + 2 * 133px);
margin-left: -133px;
margin-top: -133px;
}
.opener img.flyer {
position: absolute;
z-index: 1;
top: -1000px;
max-height: 133px;
max-width: 133px;
}
</style>
<body>
<div class="opener"></div>
</body>
<script src="//cdnjs.cloudflare.com/ajax/libs/d3/3.5.6/d3.min.js" charset="utf-8"></script>
<script src="//cdn.rawgit.com/gka/d3-jetpack/master/d3-jetpack.js" charset="utf-8"></script>
<script src="timerControl.js" charset="utf-8"></script>
<script>
Number.prototype.mod = function(n) {
return ((this%n)+n)%n;
};
driftingFlyers(d3.select('.opener'));
function driftingFlyers(container) {
var mousemoves = [];
var mouseSpeed = 0;
var mouseVector = [0,0];
container.on('mousemove', function() {
mousemoves.push({t: Date.now(), pt: d3.mouse(this)});
if(mousemoves.length > 100) mousemoves.shift();
});
var gravity = [0, .1];
var frictionCoef = 0.01;
var mouseEffectScale = d3.scale.linear()
.domain([0,700])
.range([1,0])
.clamp(true);
var flyersData = d3.range(20).map(function(d) {
return {
x: container.node().offsetWidth * Math.random(),
y: container.node().offsetHeight * Math.random(),
r: Math.PI/2,
dx: rand(10),
dy: 5,
dr: rand(Math.PI/32)
}
})
var flyer = container.selectAll('img.flyer')
.data(flyersData)
.enter()
.append('img.flyer')
.attr('src', 'flyer.png');
var driftFlyersTimer = new TimerControl(function(t) {
// calculate mouse velocity vector
var now = Date.now();
var mouseSamples = mousemoves.filter(function(d) { return now - d.t < 400 });
if(mouseSamples.length > 1) {
var s0 = mouseSamples[0];
var s1 = mouseSamples[mouseSamples.length-1];
mouseSpeed = distance(s1.pt, s0.pt) / (s1.t - s0.t);
if(isNaN(mouseSpeed)) mouseSpeed = 0; // ugh this is bad, it's more like infinity but...
mouseVector = normalize(s1.pt, s0.pt).map(function(d) { return d * mouseSpeed; });
if(isNaN(mouseVector[0])) mouseVector = [0,0];
} else {
mouseSpeed = 0;
mouseVector = [0,0];
}
// animate falling flyers
container.selectAll('img.flyer')
.each(function(d,i) {
if(mousemoves.length > 1) {
var distanceToMouse = distance([d.x, d.y], mousemoves[mousemoves.length-1].pt);
d.dx += mouseVector[0] * mouseEffectScale(distanceToMouse);
d.dy += mouseVector[1] * mouseEffectScale(distanceToMouse);
}
d.dx += gravity[0];
d.dy += gravity[1];
var frictionVector = [d.dx, d.dy].map(function(d) {
return -d * frictionCoef;
});
d.dx += frictionVector[0];
d.dy += frictionVector[1];
d.x += d.dx;
d.y += d.dy;
d.r += d.dr;
d.x = d.x.mod(container.node().offsetWidth);
d.y = d.y.mod(container.node().offsetHeight);
})
.style('left', function(d) { return d.x + 'px'; })
.style('top', function(d) { return d.y + 'px'; })
.style('transform', function(d) { return 'rotate(' + d.r + 'rad)'; });
});
driftFlyersTimer.play();
}
function rand(amplitude) {
return amplitude * (Math.random()-0.5);
}
function distance(a,b) {
var x = b[0] - a[0];
var y = b[1] - a[1];
return Math.sqrt(Math.pow(x,2) + Math.pow(y,2));
}
function difference(a,b) {
return d3.zip(a,b).map(function(x) {
return x.reduce(function(a, b) {
return a - b;
});
});
}
function normalize(a,b) {
return difference(a,b).map(function(x) {
return x/distance(a,b);
});
}
</script>
</html>
// `fn` will be called with context `context` if supplied (optional)
function TimerControl(fn, context) {
var epoch = 0;
var running = false;
// initialize, effectively
fn.call(context || this, epoch);
this.play = function() {
if(running) return;
running = true;
d3.timer(function(t) {
fn.call(context || this, epoch + t);
if (!running) {
epoch += t;
return true;
}
return false;
})
}
this.pause = function() {
running = false;
}
this.reset = function() {
epoch = 0;
fn.call(context || this, epoch);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment