Skip to content

Instantly share code, notes, and snippets.

@tzengerink
Last active August 29, 2015 14:27
Show Gist options
  • Save tzengerink/556357d00c5db88d6db2 to your computer and use it in GitHub Desktop.
Save tzengerink/556357d00c5db88d6db2 to your computer and use it in GitHub Desktop.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Light Tunnel</title>
</head>
<body>
<script type="text/javascript" src="http://d3js.org/d3.v3.min.js"></script>
<script type="text/javascript" src="light-tunnel.js"></script>
</body>
</html>
/*jshint latedef: nofunc */
(function(window) {
'use strict';
var centerCoords, debug, options, Math, d3, parseFloat, svg;
d3 = window.d3;
debug = false;
parseFloat = window.parseFloat;
Math = window.Math;
options = {
colors: [
'#f0f',
'#93f',
'#36f',
'#0cf',
'#0cc',
'#0f0',
'#ff3',
'#f60',
'#c33'
], // Sequence of colors used for lines
height: 500, // Height of the SVG
width: 960 // Width of the SVG
};
centerCoords = [options.width / 2, options.height / 2];
// Append the root SVG element
svg = d3.select('body').append('svg')
.attr('width', options.width)
.attr('height', options.height);
// Draw a background rectangle
svg.append('rect')
.attr('x', 0)
.attr('y', 0)
.attr('width', options.width)
.attr('height', options.height)
.attr('fill', '#000');
animateBendedLines(svg.append('g'));
/**
* Animated bended lines.
*
* @param {Object} group element
*/
function animateBendedLines(group) {
animatePaths();
doGroupTransitions();
/**
* Animate all paths.
*/
function animatePaths() {
var duration, ease, endR, generator, i, middleCoords,
middlePathCoords, middlePathR, middleR, numberOfLines, path,
startCoords, startR;
numberOfLines = 24;
duration = 2000;
ease = 'linear';
generator = d3.svg.line()
.x(function(d) { return d.x; })
.y(function(d) { return d.y; })
.interpolate('cardinal');
startR = d3.min([options.height, options.width]) * 0.17;
middlePathR = d3.min([options.height, options.width]) * 0.13;
middleR = startR * 1.9;
endR = Math.sqrt(Math.pow(centerCoords[0], 2) + Math.pow(centerCoords[1], 2)) + startR;
if (debug) {
endR = d3.min([options.height, options.width]) / 2;
}
makeBlurFilter();
resetCoords();
if (debug) {
makeDebugElements();
}
for (i = 0; i < numberOfLines; i++) {
makePath(i);
}
doTransition();
function dashArray(len) {
return len * 0.25 + ' ' + len * 0.025;
}
function doTransition() {
resetCoords();
group.selectAll('path')
.transition()
.duration(duration)
.ease(ease)
.attr('d', function(d, i) {
return makeD(i);
})
.attr('stroke-dasharray', function() {
return dashArray(d3.select(this).node().getTotalLength());
})
.attrTween('stroke-dashoffset', function() {
var len = d3.select(this).node().getTotalLength();
return function(t) {
return 1.2 * t * len;
};
})
.call(endAll, doTransition);
doTransitionDebugElements();
}
function doTransitionDebugElements() {
group.selectAll('circle.start')
.transition()
.ease(ease)
.duration(duration)
.attr('cx', startCoords[0])
.attr('cy', startCoords[1]);
group.selectAll('circle.middle')
.transition()
.ease(ease)
.duration(duration)
.attr('cx', middleCoords[0])
.attr('cy', middleCoords[1]);
group.selectAll('circle.end')
.transition()
.ease(ease)
.duration(duration)
.attr('cx', centerCoords[0])
.attr('cy', centerCoords[1]);
group.selectAll('circle.middle-path')
.transition()
.ease(ease)
.duration(duration)
.attr('cx', middlePathCoords[0])
.attr('cy', middlePathCoords[1]);
}
function makeBlurFilter() {
group.append('filter')
.attr('id', 'blur')
.append('feGaussianBlur')
.attr('in', 'SourceGraphic')
.attr('stdDeviation', 2);
}
function makeD(i) {
var rad = Math.PI * 2 * i / numberOfLines;
return generator([{
x: centerCoords[0] + (endR * Math.cos(rad)),
y: centerCoords[1] + (endR * Math.sin(rad))
}, {
x: middleCoords[0] + (middleR * Math.cos(rad)),
y: middleCoords[1] + (middleR * Math.sin(rad))
}, {
x: startCoords[0] + (startR * Math.cos(rad)),
y: startCoords[1] + (startR * Math.sin(rad))
}]);
}
function makeDebugElements() {
var debugGroup = group.append('g')
.attr('fill-opacity', 0)
.attr('opacity', 0.6)
.attr('stroke-width', 2);
debugGroup.append('circle')
.attr('class', 'start')
.attr('stroke', 'red')
.attr('cx', startCoords[0])
.attr('cy', startCoords[1])
.attr('r', startR);
debugGroup.append('circle')
.attr('class', 'middle')
.attr('stroke', 'red')
.attr('cx', middleCoords[0])
.attr('cy', middleCoords[1])
.attr('r', middleR);
debugGroup.append('circle')
.attr('class', 'end')
.attr('stroke', 'purple')
.attr('cx', centerCoords[0])
.attr('cy', centerCoords[1])
.attr('r', endR);
debugGroup.append('circle')
.attr('class', 'middle-path')
.attr('stroke', 'blue')
.attr('cx', middlePathCoords[0])
.attr('cy', middlePathCoords[1])
.attr('r', middlePathR);
}
function makePath(i) {
path = group.append('path')
.attr('opacity', 0.8)
.attr('filter', 'url(#blur)')
.attr('fill-opacity', 0)
.attr('stroke-width', 8)
.attr('d', makeD(i))
.attr('stroke-dasharray', function() {
return dashArray(d3.select(this).node().getTotalLength());
})
.attr('stroke-dashoffset', 0);
}
function resetCoords() {
startCoords = randomCoordsOnCircle(centerCoords, endR);
middlePathCoords = randomCoordsOnCircle(centerCoords, middlePathR);
middleCoords = randomCoordsOnCircle(middlePathCoords, middlePathR);
}
}
/**
* Transition the colors of the group.
*/
function doGroupTransitions() {
group.transition()
.ease('linear')
.duration(10000)
.attrTween('stroke', tweenColor)
.each('end', doGroupTransitions);
/**
* Custom attribute tween used for the color transitions.
*/
function tweenColor() {
return function(t0) {
var end, start, t1;
if (t0 >= 1) {
return options.colors[options.colors.length - 1];
}
t1 = t0 * (options.colors.length - 1);
start = options.colors[Math.floor(t1)];
end = options.colors[Math.floor(t1) + 1];
return d3.interpolateHsl(start, end)(t1 % 1);
};
}
}
}
/**
* Execute a callback when a transition has ended for all elements.
*
* @param {Object} transition
* @param {Function} callback
*/
function endAll(transition, callback) {
var n = 0;
transition.each(function() { ++n; })
.each('end', function() {
if ( ! --n) {
callback.apply(this, arguments);
}
});
}
/**
* Create a random number between 0 (inclusive) and max (exclusive).
*
* @param {Number} max
* @param {Number} min (default to 0)
*
* @return {Number} random
*/
function random(max, min) {
min = min || 0;
return (Math.random() * (max - min) + min);
}
/**
* Get random coordinates on a virtual circle.
*
* @param {Array} startCoords
* @param {Number} r
*
* @return {Array} coords [x, y]
*/
function randomCoordsOnCircle(startCoords, r) {
var rad = random(2 * Math.PI);
return [startCoords[0] + (r * Math.cos(rad)),
startCoords[1] + (r * Math.sin(rad))];
}
})(window);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment