Skip to content

Instantly share code, notes, and snippets.

@robinhouston
Created August 4, 2013 17:49
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 robinhouston/6151172 to your computer and use it in GitHub Desktop.
Save robinhouston/6151172 to your computer and use it in GitHub Desktop.
Experiments in flow

An experiment in flow visualisation and the mathematical transformation of vector fields. Visual style obviously influenced by the classic http://hint.fm/wind/

<!DOCTYPE html>
<meta charset="utf-8">
<title>Spiral flow</title>
<script src="http://bl.ocks.org/robinhouston/raw/6096562/rAF.js" charset="utf-8"></script>
<script src="streamer.js" charset="utf-8"></script>
<style>
canvas { background-color: #4C4C4C; }
</style>
<canvas width=960 height=500></canvas>
<script>
var NUM_STREAMS = 8000,
MAX_AGE = 50,
FADE_RATE = 0.05,
BORDER = 100;
var canvas = document.getElementsByTagName("canvas")[0],
cx = canvas.getContext("2d"),
streamer = Streamer({
canvas: canvas,
num_streams: NUM_STREAMS,
max_age: MAX_AGE,
fade_rate: FADE_RATE,
border: BORDER,
velocity: to_px(rotate(Math.PI/4, double_spiral(spiral(1/2, Math.PI/6))))
});
cx.strokeStyle = "rgba(255,255,255,0.5)";
cx.fillStyle = "rgba(255,255,255,0.05)";
cx.fillRect(0, 0, canvas.width, canvas.height);
streamer.start();
// * Vector field transformers
// Rotate the vector field by θ
function rotate(θ, f) {
var cos_θ = Math.cos(θ), sin_θ = Math.sin(θ);
return function(x, y, t) {
return f(x*cos_θ - y*sin_θ, x*sin_θ + y*cos_θ, t);
};
}
// Transform the vector field f under the mobius transformation z ↦ (z-1)/(z+1)
function double_spiral(f) {
return function(x, y, t) {
// Let (x0, y0) be the inverse image of (x, y) ...
var r0 = (x-1)*(x-1) + y*y,
x0 = (1 - x*x - y*y) / r0,
y0 = 2*y / r0,
// Find the velocity v0 at (x0, y0)
v0 = f(x0, y0, t),
vx = v0[0], vy = v0[1];
// and hit v0 with the Jacobian at (x0, y0)
var denom = Math.pow((x0+1)*(x0+1) + y0*y0, 2) / 4,
a = 2 * (y0*y0 - (x0+1)*(x0+1)),
b = 4*(x0+1)*y0;
return [
( a * vx - b * vy ) / denom,
( b * vx + a * vy ) / denom
];
};
}
// Transform a vector field from origin-centred coordinates to pixel coords
function to_px(f) {
return function(x_px, y_px, t) {
var divisor = Math.min(canvas.width, canvas.height)/4,
x = (x_px - canvas.width/2) / divisor,
y = (y_px - canvas.height/2) / divisor;
return f(x, y, t);
};
}
// * Primitive vector fields
function spiral(r, theta) {
var log_r = Math.log(r);
return function (x, y, t) {
return [
x*log_r - y*theta,
x*theta + y*log_r
];
};
}
</script>
function Streamer(options) {
if (!options.hasOwnProperty("canvas")) throw "The 'canvas' option must be specified";
if (!options.hasOwnProperty("velocity")) throw "The 'velocity' option must be specified";
var canvas = options.canvas,
cx = canvas.getContext("2d"),
w = canvas.width, h = canvas.height,
num_streams = options.num_streams || 1000,
ms_per_repeat = options.ms_per_repeat || 1000,
max_age = options.max_age || 10,
fade_rate = options.fade_rate || 0.02,
border = options.border || 100,
velocity = options.velocity;
function fadeCanvas(alpha) {
cx.save();
cx.globalAlpha = alpha;
cx.globalCompositeOperation = "copy";
cx.drawImage(canvas, 0, 0);
cx.restore();
}
var streams = initStreams();
function initStreams() {
var streams = [];
for (var i=0; i<num_streams; i++)
streams.push([
Math.round(Math.random() * (w + 2*border)) - border,
Math.round(Math.random() * (h + 2*border)) - border,
Math.round(Math.random() * max_age) + 1
]);
return streams;
}
function frame(t) {
fadeCanvas(1 - fade_rate);
cx.save();
cx.setTransform(1, 0, 0, 1, 0, 0);
for (var i=0; i<streams.length; i++) {
var stream = streams[i];
if (stream[2] == 0) {
stream[0] = Math.round(Math.random() * (w + 2*border)) - border;
stream[1] = Math.round(Math.random() * (h + 2*border)) - border;
stream[2] = MAX_AGE;
}
var v = velocity(stream[0], stream[1], t);
if (v[0] <= w && v[1] <= h) {
cx.beginPath();
cx.moveTo(stream[0], stream[1]);
cx.lineTo(stream[0] + v[0], stream[1] + v[1]);
cx.stroke();
stream[0] += v[0];
stream[1] += v[1];
}
stream[2]--;
}
cx.restore();
}
var first_timestamp, animation_frame_id;
function loop(timestamp) {
if (!first_timestamp) first_timestamp = timestamp;
frame(((timestamp - first_timestamp) % ms_per_repeat) / ms_per_repeat);
animation_frame_id = requestAnimationFrame(loop);
}
return {
"start": function() { animation_frame_id = requestAnimationFrame(loop); },
"stop": function() { cancelAnimationFrame(animation_frame_id); }
};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment