An experiment in flow visualisation and the mathematical transformation of vector fields. Visual style obviously influenced by the classic http://hint.fm/wind/
Created
August 4, 2013 17:49
-
-
Save robinhouston/6151172 to your computer and use it in GitHub Desktop.
Experiments in flow
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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