Skip to content

Instantly share code, notes, and snippets.

@ejb
Forked from anonymous/index.html
Last active September 21, 2015 11:04
Show Gist options
  • Save ejb/5de9c86f6a69e90d07b6 to your computer and use it in GitHub Desktop.
Save ejb/5de9c86f6a69e90d07b6 to your computer and use it in GitHub Desktop.
JS Bin // source https://jsbin.com/baxoti
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>JS Bin</title>
<style id="jsbin-css">
.chapter-illo {
width: 100%;
}
</style>
</head>
<body>
<div class="chapter-illo">
<!-- inline SVG -->
<svg version="1.1" id="Layer_6" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="840.24px" height="592.8px" viewBox="0 0 840.24 592.8" enable-background="new 0 0 840.24 592.8" xml:space="preserve">
<image xlink:href="http://graphics.wsj.com/libor-unraveling-tom-hayes/img/chapter-illos/chapter1-bg.jpg" x="0" y="0" height="592.8px" width="840.24px" image-rendering="optimizeQuality" />
<path fill="none" stroke="#EB6824" stroke-width="3" stroke-miterlimit="10" d="M815.787,589.919l-20.333-113.186l-59.333-81.333
l-93.333-86.999c0,0-6.334-64.334-13.667-89.667s-18.333-53-25.333-61.333s-23.666-11.999-22.667-14.667s10.667,0.334,19-2.333
s12.334-11.332,11.333-14.333s-15.666-0.667-32.666-6.333s-47-23.333-47-23.333s-20.746-2.063-33.334-4c-13-2-10.834-0.332-14.334,0
s-38.5-17.666-40-16.333s7.167,29.501,5.667,30s-9.334-15.667-17.667-10s-52,39-58.667,43S334.62,200.9,335.12,195.735
s23.667,113.333,23.667,113.333l46-5.666l21-62.333c0,0,17.833-19.499,20-19.667s59.333,26.002,60.667,25.333
s-9.666-25.667-8.333-29s12.332,14.667,17.666,17.667s20.667,11.835,22.667,11s3.054-40.802,5.11-40.568
s39.389,29.903,40.223,28.568s-5.167-14.832-5.333-17s27.5-21.332,25.667-23.333s-15.501,7.834-17.667,8.333
s-30.668-14.665-31.667-13.333s14.999,14.501,15,16.333s-61.666,60.667-61.666,60.667l-1,19.667l-13.334,65c0,0,20,0.667,41,4.667
s31,13.333,31,13.333s4.667,38.667-0.333,42.667s-14.642,4.307-14.333,10.667c0.204,4.201,1.999,10.667,15.333,11
s26.333,13.333,26.333,13.333s3.334,1.334,5.667-2.333s1.001-3,3.667-5s2.999-0.334,4.666-4.667s-0.666-2,1.334-6.333
s5.666-13.334,2.666-18.667s-7-5-7-5l-13.333-49.666l13-17c0,0,1.703-0.846,5.167-4.742c0,0,2.574-0.698,2.637-2.015
s-0.752-4.263-0.376-5.329s2.32-3.512,3.448-4.515s3.385,0.753,4.765,0.439s16.678-20.69,16.678-20.69s1.755,0.126,2.257,1.631
s-0.753,5.643-2.947,8.777s-11.224,12.791-11.224,12.791s-2.381,1.379-4.89,4.2s-12.289,13.669-12.289,13.669
s2.571,5.016,5.267,4.953s3.763-0.689,6.145-3.197s3.135-4.892,5.016-6.082s6.27-2.257,6.27-2.257s1.565-0.251,1.003-3.072
c-0.188-0.94-0.501-1.756-0.501-1.756l-3.512,1.258c0,0-2.319,3.068-3.26,3.068s-1.128-0.376-1.567-0.689s-2.947-0.112-2.383-1.185
s1.943-1.573,3.386-2.326s11.724-13.354,11.724-13.354s2.508-2.258,4.64-1.631s3.01,2.759,3.01,2.759s3.446,22.195,3.259,23.198
s-1.553,13.51-1.553,17.51s1.25,31.25,1.75,34.25s-8.125,41.125-11,46.125s-9.5,16.75-13.5,17.5s-25.125,2.75-25.125,2.75
s-3.25,0.125-4.375,1s-3,3.125-3,3.125s0,2.375-2.625,2.75s-21.5,1.375-21.5,1.375L551.62,473.9l1.875,3.5c0,0,6.375-1,10,0.375
s4.875,4,8.875,4.375s10.625-2.25,12.75-2.5s9.875-2.75,11.875-4.75s11.75-12,13.75-12.5s8.75,6,8.625,7.5
s-4.625,14.625-4.625,14.625s-3.75,12.25-5.5,14.125s-21,9.25-21,9.25s-26.375,8.25-29.875,6.375s-20.25-14-20.25-14
s-17.75-10.75-20.625-14.25s-8.875-9.875-8.875-9.875s-3.375-7.375-6-9.25s-25.375-18.125-25.375-18.125
c-0.25-2-7.5-34-8.875-36.875s-16.875-15.5-18.375-18.75s-4.375-10.5-3.625-12s15.625-10.625,15.625-10.625l15.75-7.875l0.125,2.5
c0,0,2.25,1.375,3.75,1s10.375-6.5,12.25-6.375s2.5,0.625,3.25,1.5s11.125,10.625,11.125,10.625s0.625,2.75,3.125,2.875
s3.375,2.5,4.25,2.875s3.375,0.625,5.5-0.5s7.125-7.375,7.25-8.375s-3,0.5-4.625,1.375s-2.75,0.875-4.5,0.375s-2.375-1.5-4-0.875
s-12.625,5.75-14,5.875s-2-0.25-1.875,1s3.25,1.25,5.75,1.625s3,0.5,7,2.75s6.625,5,8,4.625s7.625-3.25,8.625-3.5s6.5-1,7.125-2.625
s3.25-6.75,1.625-6.625s-6,2-5.125-0.75s3.375-3.625,5.75-3.5s4.5-0.75,4.625-1.5s1.375-1.25-0.375-1.5s-16.625-5.25-18.875-5.125
s-5.75,2.125-9.375,2s-15-1.25-18-3s-65.125-63.875-65.125-63.875s2.624,18.042,1.458,23.875s-8.74,18.168-9.453,23.334
s-5.185,40.507-7.046,47.333c0,0,0.5,0.834,0.25,1.334s-1.333,0.499-2.417,1.666s-0.167,4.666-2.167,5.5s-3.25,1.251-5.25-0.999
s-27-36.917-27-36.917s-2.083-6.833-3.417-9.5s-12.167-8.416-13.5-11.25s-7.083-12.667-6.333-14.417s6-5.833,6-5.833
s4.583-3.251,6.833-2.834s14,5.834,14,5.834s11.583,4.583,13.083,6.916s10.167,13.668,10.833,16.584s26.917,85.416,26.917,85.416
s3.333,9.667,5.833,12.167s34.834,26.5,38.5,28.5s48.667,32.833,51.667,35s6.833,6,6.833,6l-59.166,31.833l-17.667,18
c0,0-7.833-67.166-8.833-70.333S341.787,321.401,341.787,321.401L29.62,477.233"/>
</svg>
<p>Original illustration by the Gentleman Draughtsman.</p>
<p>See this effect in situ in <a href="http://graphics.wsj.com/libor-unraveling-tom-hayes/">The Unraveling of Tom Hayes</a>.</p>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/snap.svg/0.4.1/snap.svg-min.js"></script>
<script src="https://code.jquery.com/jquery-2.1.4.js"></script>
<script tyle="text/javascript" src="https://rawgit.com/subprotocol/verlet-js/master/js/verlet-1.0.0.min.js"></script>
</div>
<script id="jsbin-javascript">
/*
This is a breakdown of the effect used in 'The Unraveling of Tom Hayes'.
See it in situ: http://graphics.wsj.com/libor-unraveling-tom-hayes/
What this does:
- Animates dash offset of multiple <path>s within an SVG
- Adds a physics-enabled 'thread' which follows dash offset progress
Technologies used:
- SVG controlled by Snap.svg (http://snapsvg.io)
- HTML5 Canvas with Vertlet-JS physics engine (https://github.com/subprotocol/verlet-js)
Additional features _not_ included in this code:
- Replacing a static JPEG with SVG image using AJAX (progressive enhancement!)
- and load SVG background image first, for seamless transition from JPG to SVG
- Trigger animation when illustration scrolls into view
- Deactivate animation if offscreen (to improve performance)
*/
// kick it all off
var u = unraveling($('svg')).init();
// this function controls the SVG animation
function unraveling($svg){
if (oldFF()){
return;
}
var snap = Snap($svg[0]);
var w = +$svg.attr('width').replace('px','');
var h = w/2;
if (snap.node.offsetWidth > w) {
w = snap.node.offsetWidth;
}
var animationLength = 10000;
var paths = snap.selectAll('path');
// loop through every path in the SVG
// these make up the Hayes portrait
var pathData = [];
var totalLength = 0;
paths.forEach(function(path){
// store data for each path
// this will come in handy later
var length = path.getTotalLength();
pathData.push({
d: path.attr('d'),
length: length
});
// set each path's stroke pattern to a single,
// full-length line
path.attr({
"stroke-dasharray": "0 " + length + " " + length,
"stroke-dashoffset": length,
"stroke-linecap": "round"
});
totalLength += length;
// add a 'remainder' path underneath
// which will be revealed when unraveled
var remainderColor = 'white';
snap.select('image').after(
Snap($svg[0]).path(path.attr('d')).attr({
opacity: 0.9,
fill: 'none',
stroke: remainderColor,
'stroke-width': path.attr('stroke-width'),
"stroke-linecap": "round"
})
);
}).attr({
fill: 'none'
});
// calculate percentage of unraveling
// each path represents
for (var i = 0; i < pathData.length; i++) {
pathData[i].perc = pathData[i].length/totalLength;
}
var firstPoint = paths[0].getPointAtLength(0);
// this function runs the animation
// animOpts takes two properties:
// - onMove: callback for moving physics-enabled thread
// - onEnd: callback for unpinning thread
function animatePath(animOpts){
var onMove = animOpts.onMove;
var i = 0;
anim();
function anim(){
var path = paths[i];
moveToPoint(path, onMove);
path.animate({
// animated dashoffset to give impression
// that line is disappearing
"stroke-dashoffset": 10
}, (animationLength*pathData[i].perc), function() {
// when finished, make line invisible
path.attr('opacity', 0);
i++;
if (paths[i]) {
// run animation for next path
anim();
} else {
// unpin thread for finish
// or it'll hang awkwardly
animOpts.onEnd();
}
});
}
}
// this controls where the 'loose thread' is pinned to
function moveToPoint(path,callback){
Snap.animate(0, totalLength, function(value) {
movePoint = path.getPointAtLength(value);
if (!isNaN(movePoint.x)) {
callback(movePoint.x,movePoint.y);
}
}, animationLength);
}
// new instance of the physics-enabled thread
var threadMove = Thread({
// use the illustration SVG's actual color
color: paths[0].attr('stroke'),
svg: $svg,
start: {
x: paths[0].getPointAtLength(0).x,
y: paths[0].getPointAtLength(0).y
},
// prevents thread from 'pooling' at the bottom
noPooling: false
});
return {
init: function(){
threadMove.init();
animatePath({
onMove: threadMove.move,
onEnd: threadMove.end
});
}
};
} // end unraveling function
// this controls the canvas-based, physics-enabled dangling thread
function Thread(opts) {
// canvas dimensions
var width = +opts.svg.outerWidth();
var height = +opts.svg.outerHeight();
// match coordinate system to our viewBox'd SVG
var mod = {
x: +opts.svg.eq(0).attr('width').replace('px',''),
y: +opts.svg.eq(0).attr('height').replace('px','')
};
opts = opts || {};
opts.color = opts.color || 'black';
opts.start = opts.start || {x:width/2,y:height};
// VertletJS uses a regular HTML5 canvas
// which in this case goes over the top of the SVG
var canvas = document.createElement('canvas');
canvas.style.position = 'absolute';
canvas.style.display = 'inline-block';
canvas.style.textAlign = 'right';
opts.svg.before(canvas);
// retina stuff
var dpr = window.devicePixelRatio || 1;
canvas.width = width*dpr;
canvas.height = height*dpr;
canvas.getContext("2d").scale(dpr, dpr);
canvas.width = width;
canvas.height = height;
// create a new physics simulation
// using VertletJS
var sim = new VerletJS(width, height, canvas);
sim.friction = 0.95;
sim.gravity = new Vec2(0,1.5);
opts.start.x = (opts.start.x/mod.x)*width;
opts.start.y = (opts.start.y/mod.y)*height;
// some useful configuration variables
var threadLength = 2;
var joinLength = 10;
var lineWidth = 2;
// reduce complexity if on mobile
// to increase performance
if ($(window).width() < 992) {
joinLength = 15;
lineWidth = 1;
threadLength = 1;
}
// create entities - ie, the points of the thread
// (which is actually several tiny circles linked together
// with straight lines)
var stiffness = 6;
var vecs = [];
for (var i = 0; i < 10; i++) {
var xx = opts.start.x;
var yy = opts.start.y+(i*joinLength);
vecs.push( new Vec2(xx,yy) );
}
// combine them into a single line
var segment = sim.lineSegments(vecs, stiffness);
// pin the end of the thread so it dangles
var pin = segment.pin(vecs.length-1);
// control how the thread is styled
// using the regular canvas API
segment.drawConstraints = function(ctx, composite) {
ctx.beginPath();
ctx.moveTo(composite.particles[0].pos.x, composite.particles[0].pos.y);
for (var i = 1; i < composite.particles.length; i++) {
if(composite.particles[i]) {
ctx.lineTo(composite.particles[i].pos.x, composite.particles[i].pos.y);
}
}
ctx.lineWidth = lineWidth;
ctx.strokeStyle = opts.color;
ctx.stroke();
};
segment.drawParticles = function(ctx, composite) {
// draw nothing for particles
};
// animation loop
var loopIndex = 0;
var loop = function() {
var ctx = canvas.getContext('2d');
ctx.clearRect(0,0,10000,10000);
sim.frame(16);
sim.draw();
loopIndex++;
requestAnimFrame(loop);
};
// prevent the thread from getting too long
// especially on mobile
var limit = 150;
if ($(window).width() < 992) {
limit = 50;
}
return {
init: loop,
move: function(x,y){
// this function is called during the unraveling() function
// it moves the pinned particle and increases
// the thread length, giving the illusion of unraveling
var first = segment.particles[0];
var last = segment.particles[segment.particles.length-1];
// only increase it once every 5 loops,
// and only until 150
if ((loopIndex%5 === 0) && (segment.particles.length < 150) && first && first.lastPos) {
var pY = first.lastPos.y+2;
// make sure it doesn't drop off the bottom
// or the thread risks leaping around like a snake
if (pY > sim.height+4) {
pY = sim.height-1;
}
// add a new particle on the end
// to increase thread length
var newParticle = new Particle({
x: first.lastPos.x,
y: pY
});
segment.particles.unshift(newParticle);
// we need to modify the constrainsts between particles
var newConstraint = new DistanceConstraint( newParticle , segment.particles[1], 2);
segment.constraints.unshift(newConstraint);
}
// move pinned particle
pin.pos.x = (x/mod.x)*width;
pin.pos.y = (y/mod.y)*height;
},
end: function(){
// optional: make thread drop out the bottom
if (opts.noPooling){
sim.bounds = function(){};
}
// when finished, remove pinned particle
delete segment.constraints[segment.constraints.length-1];
delete segment.particles[segment.particles.length-1];
pin.relax();
}
};
}
/* Check if Firefox version is below 40, which has an SVG bug */
function oldFF(){
var ffversion = window.navigator.userAgent.split('Firefox/')[1];
if (ffversion && (+ffversion < 40)){
return true;
} else {
return false;
}
}
</script>
<script id="jsbin-source-css" type="text/css">.chapter-illo {
width: 100%;
> svg {
}
}</script>
<script id="jsbin-source-javascript" type="text/javascript">/*
This is a breakdown of the effect used in 'The Unraveling of Tom Hayes'.
See it in situ: http://graphics.wsj.com/libor-unraveling-tom-hayes/
What this does:
- Animates dash offset of multiple <path>s within an SVG
- Adds a physics-enabled 'thread' which follows dash offset progress
Technologies used:
- SVG controlled by Snap.svg (http://snapsvg.io)
- HTML5 Canvas with Vertlet-JS physics engine (https://github.com/subprotocol/verlet-js)
Additional features _not_ included in this code:
- Replacing a static JPEG with SVG image using AJAX (progressive enhancement!)
- and load SVG background image first, for seamless transition from JPG to SVG
- Trigger animation when illustration scrolls into view
- Deactivate animation if offscreen (to improve performance)
*/
// kick it all off
var u = unraveling($('svg')).init();
// this function controls the SVG animation
function unraveling($svg){
if (oldFF()){
return;
}
var snap = Snap($svg[0]);
var w = +$svg.attr('width').replace('px','');
var h = w/2;
if (snap.node.offsetWidth > w) {
w = snap.node.offsetWidth;
}
var animationLength = 10000;
var paths = snap.selectAll('path');
// loop through every path in the SVG
// these make up the Hayes portrait
var pathData = [];
var totalLength = 0;
paths.forEach(function(path){
// store data for each path
// this will come in handy later
var length = path.getTotalLength();
pathData.push({
d: path.attr('d'),
length: length
});
// set each path's stroke pattern to a single,
// full-length line
path.attr({
"stroke-dasharray": "0 " + length + " " + length,
"stroke-dashoffset": length,
"stroke-linecap": "round"
});
totalLength += length;
// add a 'remainder' path underneath
// which will be revealed when unraveled
var remainderColor = 'white';
snap.select('image').after(
Snap($svg[0]).path(path.attr('d')).attr({
opacity: 0.9,
fill: 'none',
stroke: remainderColor,
'stroke-width': path.attr('stroke-width'),
"stroke-linecap": "round"
})
);
}).attr({
fill: 'none'
});
// calculate percentage of unraveling
// each path represents
for (var i = 0; i < pathData.length; i++) {
pathData[i].perc = pathData[i].length/totalLength;
}
var firstPoint = paths[0].getPointAtLength(0);
// this function runs the animation
// animOpts takes two properties:
// - onMove: callback for moving physics-enabled thread
// - onEnd: callback for unpinning thread
function animatePath(animOpts){
var onMove = animOpts.onMove;
var i = 0;
anim();
function anim(){
var path = paths[i];
moveToPoint(path, onMove);
path.animate({
// animated dashoffset to give impression
// that line is disappearing
"stroke-dashoffset": 10
}, (animationLength*pathData[i].perc), function() {
// when finished, make line invisible
path.attr('opacity', 0);
i++;
if (paths[i]) {
// run animation for next path
anim();
} else {
// unpin thread for finish
// or it'll hang awkwardly
animOpts.onEnd();
}
});
}
}
// this controls where the 'loose thread' is pinned to
function moveToPoint(path,callback){
Snap.animate(0, totalLength, function(value) {
movePoint = path.getPointAtLength(value);
if (!isNaN(movePoint.x)) {
callback(movePoint.x,movePoint.y);
}
}, animationLength);
}
// new instance of the physics-enabled thread
var threadMove = Thread({
// use the illustration SVG's actual color
color: paths[0].attr('stroke'),
svg: $svg,
start: {
x: paths[0].getPointAtLength(0).x,
y: paths[0].getPointAtLength(0).y
},
// prevents thread from 'pooling' at the bottom
noPooling: false
});
return {
init: function(){
threadMove.init();
animatePath({
onMove: threadMove.move,
onEnd: threadMove.end
});
}
};
} // end unraveling function
// this controls the canvas-based, physics-enabled dangling thread
function Thread(opts) {
// canvas dimensions
var width = +opts.svg.outerWidth();
var height = +opts.svg.outerHeight();
// match coordinate system to our viewBox'd SVG
var mod = {
x: +opts.svg.eq(0).attr('width').replace('px',''),
y: +opts.svg.eq(0).attr('height').replace('px','')
};
opts = opts || {};
opts.color = opts.color || 'black';
opts.start = opts.start || {x:width/2,y:height};
// VertletJS uses a regular HTML5 canvas
// which in this case goes over the top of the SVG
var canvas = document.createElement('canvas');
canvas.style.position = 'absolute';
canvas.style.display = 'inline-block';
canvas.style.textAlign = 'right';
opts.svg.before(canvas);
// retina stuff
var dpr = window.devicePixelRatio || 1;
canvas.width = width*dpr;
canvas.height = height*dpr;
canvas.getContext("2d").scale(dpr, dpr);
canvas.width = width;
canvas.height = height;
// create a new physics simulation
// using VertletJS
var sim = new VerletJS(width, height, canvas);
sim.friction = 0.95;
sim.gravity = new Vec2(0,1.5);
opts.start.x = (opts.start.x/mod.x)*width;
opts.start.y = (opts.start.y/mod.y)*height;
// some useful configuration variables
var threadLength = 2;
var joinLength = 10;
var lineWidth = 2;
// reduce complexity if on mobile
// to increase performance
if ($(window).width() < 992) {
joinLength = 15;
lineWidth = 1;
threadLength = 1;
}
// create entities - ie, the points of the thread
// (which is actually several tiny circles linked together
// with straight lines)
var stiffness = 6;
var vecs = [];
for (var i = 0; i < 10; i++) {
var xx = opts.start.x;
var yy = opts.start.y+(i*joinLength);
vecs.push( new Vec2(xx,yy) );
}
// combine them into a single line
var segment = sim.lineSegments(vecs, stiffness);
// pin the end of the thread so it dangles
var pin = segment.pin(vecs.length-1);
// control how the thread is styled
// using the regular canvas API
segment.drawConstraints = function(ctx, composite) {
ctx.beginPath();
ctx.moveTo(composite.particles[0].pos.x, composite.particles[0].pos.y);
for (var i = 1; i < composite.particles.length; i++) {
if(composite.particles[i]) {
ctx.lineTo(composite.particles[i].pos.x, composite.particles[i].pos.y);
}
}
ctx.lineWidth = lineWidth;
ctx.strokeStyle = opts.color;
ctx.stroke();
};
segment.drawParticles = function(ctx, composite) {
// draw nothing for particles
};
// animation loop
var loopIndex = 0;
var loop = function() {
var ctx = canvas.getContext('2d');
ctx.clearRect(0,0,10000,10000);
sim.frame(16);
sim.draw();
loopIndex++;
requestAnimFrame(loop);
};
// prevent the thread from getting too long
// especially on mobile
var limit = 150;
if ($(window).width() < 992) {
limit = 50;
}
return {
init: loop,
move: function(x,y){
// this function is called during the unraveling() function
// it moves the pinned particle and increases
// the thread length, giving the illusion of unraveling
var first = segment.particles[0];
var last = segment.particles[segment.particles.length-1];
// only increase it once every 5 loops,
// and only until 150
if ((loopIndex%5 === 0) && (segment.particles.length < 150) && first && first.lastPos) {
var pY = first.lastPos.y+2;
// make sure it doesn't drop off the bottom
// or the thread risks leaping around like a snake
if (pY > sim.height+4) {
pY = sim.height-1;
}
// add a new particle on the end
// to increase thread length
var newParticle = new Particle({
x: first.lastPos.x,
y: pY
});
segment.particles.unshift(newParticle);
// we need to modify the constrainsts between particles
var newConstraint = new DistanceConstraint( newParticle , segment.particles[1], 2);
segment.constraints.unshift(newConstraint);
}
// move pinned particle
pin.pos.x = (x/mod.x)*width;
pin.pos.y = (y/mod.y)*height;
},
end: function(){
// optional: make thread drop out the bottom
if (opts.noPooling){
sim.bounds = function(){};
}
// when finished, remove pinned particle
delete segment.constraints[segment.constraints.length-1];
delete segment.particles[segment.particles.length-1];
pin.relax();
}
};
}
/* Check if Firefox version is below 40, which has an SVG bug */
function oldFF(){
var ffversion = window.navigator.userAgent.split('Firefox/')[1];
if (ffversion && (+ffversion < 40)){
return true;
} else {
return false;
}
}
</script></body>
</html>
.chapter-illo {
width: 100%;
}
/*
This is a breakdown of the effect used in 'The Unraveling of Tom Hayes'.
See it in situ: http://graphics.wsj.com/libor-unraveling-tom-hayes/
What this does:
- Animates dash offset of multiple <path>s within an SVG
- Adds a physics-enabled 'thread' which follows dash offset progress
Technologies used:
- SVG controlled by Snap.svg (http://snapsvg.io)
- HTML5 Canvas with Vertlet-JS physics engine (https://github.com/subprotocol/verlet-js)
Additional features _not_ included in this code:
- Replacing a static JPEG with SVG image using AJAX (progressive enhancement!)
- and load SVG background image first, for seamless transition from JPG to SVG
- Trigger animation when illustration scrolls into view
- Deactivate animation if offscreen (to improve performance)
*/
// kick it all off
var u = unraveling($('svg')).init();
// this function controls the SVG animation
function unraveling($svg){
if (oldFF()){
return;
}
var snap = Snap($svg[0]);
var w = +$svg.attr('width').replace('px','');
var h = w/2;
if (snap.node.offsetWidth > w) {
w = snap.node.offsetWidth;
}
var animationLength = 10000;
var paths = snap.selectAll('path');
// loop through every path in the SVG
// these make up the Hayes portrait
var pathData = [];
var totalLength = 0;
paths.forEach(function(path){
// store data for each path
// this will come in handy later
var length = path.getTotalLength();
pathData.push({
d: path.attr('d'),
length: length
});
// set each path's stroke pattern to a single,
// full-length line
path.attr({
"stroke-dasharray": "0 " + length + " " + length,
"stroke-dashoffset": length,
"stroke-linecap": "round"
});
totalLength += length;
// add a 'remainder' path underneath
// which will be revealed when unraveled
var remainderColor = 'white';
snap.select('image').after(
Snap($svg[0]).path(path.attr('d')).attr({
opacity: 0.9,
fill: 'none',
stroke: remainderColor,
'stroke-width': path.attr('stroke-width'),
"stroke-linecap": "round"
})
);
}).attr({
fill: 'none'
});
// calculate percentage of unraveling
// each path represents
for (var i = 0; i < pathData.length; i++) {
pathData[i].perc = pathData[i].length/totalLength;
}
var firstPoint = paths[0].getPointAtLength(0);
// this function runs the animation
// animOpts takes two properties:
// - onMove: callback for moving physics-enabled thread
// - onEnd: callback for unpinning thread
function animatePath(animOpts){
var onMove = animOpts.onMove;
var i = 0;
anim();
function anim(){
var path = paths[i];
moveToPoint(path, onMove);
path.animate({
// animated dashoffset to give impression
// that line is disappearing
"stroke-dashoffset": 10
}, (animationLength*pathData[i].perc), function() {
// when finished, make line invisible
path.attr('opacity', 0);
i++;
if (paths[i]) {
// run animation for next path
anim();
} else {
// unpin thread for finish
// or it'll hang awkwardly
animOpts.onEnd();
}
});
}
}
// this controls where the 'loose thread' is pinned to
function moveToPoint(path,callback){
Snap.animate(0, totalLength, function(value) {
movePoint = path.getPointAtLength(value);
if (!isNaN(movePoint.x)) {
callback(movePoint.x,movePoint.y);
}
}, animationLength);
}
// new instance of the physics-enabled thread
var threadMove = Thread({
// use the illustration SVG's actual color
color: paths[0].attr('stroke'),
svg: $svg,
start: {
x: paths[0].getPointAtLength(0).x,
y: paths[0].getPointAtLength(0).y
},
// prevents thread from 'pooling' at the bottom
noPooling: false
});
return {
init: function(){
threadMove.init();
animatePath({
onMove: threadMove.move,
onEnd: threadMove.end
});
}
};
} // end unraveling function
// this controls the canvas-based, physics-enabled dangling thread
function Thread(opts) {
// canvas dimensions
var width = +opts.svg.outerWidth();
var height = +opts.svg.outerHeight();
// match coordinate system to our viewBox'd SVG
var mod = {
x: +opts.svg.eq(0).attr('width').replace('px',''),
y: +opts.svg.eq(0).attr('height').replace('px','')
};
opts = opts || {};
opts.color = opts.color || 'black';
opts.start = opts.start || {x:width/2,y:height};
// VertletJS uses a regular HTML5 canvas
// which in this case goes over the top of the SVG
var canvas = document.createElement('canvas');
canvas.style.position = 'absolute';
canvas.style.display = 'inline-block';
canvas.style.textAlign = 'right';
opts.svg.before(canvas);
// retina stuff
var dpr = window.devicePixelRatio || 1;
canvas.width = width*dpr;
canvas.height = height*dpr;
canvas.getContext("2d").scale(dpr, dpr);
canvas.width = width;
canvas.height = height;
// create a new physics simulation
// using VertletJS
var sim = new VerletJS(width, height, canvas);
sim.friction = 0.95;
sim.gravity = new Vec2(0,1.5);
opts.start.x = (opts.start.x/mod.x)*width;
opts.start.y = (opts.start.y/mod.y)*height;
// some useful configuration variables
var threadLength = 2;
var joinLength = 10;
var lineWidth = 2;
// reduce complexity if on mobile
// to increase performance
if ($(window).width() < 992) {
joinLength = 15;
lineWidth = 1;
threadLength = 1;
}
// create entities - ie, the points of the thread
// (which is actually several tiny circles linked together
// with straight lines)
var stiffness = 6;
var vecs = [];
for (var i = 0; i < 10; i++) {
var xx = opts.start.x;
var yy = opts.start.y+(i*joinLength);
vecs.push( new Vec2(xx,yy) );
}
// combine them into a single line
var segment = sim.lineSegments(vecs, stiffness);
// pin the end of the thread so it dangles
var pin = segment.pin(vecs.length-1);
// control how the thread is styled
// using the regular canvas API
segment.drawConstraints = function(ctx, composite) {
ctx.beginPath();
ctx.moveTo(composite.particles[0].pos.x, composite.particles[0].pos.y);
for (var i = 1; i < composite.particles.length; i++) {
if(composite.particles[i]) {
ctx.lineTo(composite.particles[i].pos.x, composite.particles[i].pos.y);
}
}
ctx.lineWidth = lineWidth;
ctx.strokeStyle = opts.color;
ctx.stroke();
};
segment.drawParticles = function(ctx, composite) {
// draw nothing for particles
};
// animation loop
var loopIndex = 0;
var loop = function() {
var ctx = canvas.getContext('2d');
ctx.clearRect(0,0,10000,10000);
sim.frame(16);
sim.draw();
loopIndex++;
requestAnimFrame(loop);
};
// prevent the thread from getting too long
// especially on mobile
var limit = 150;
if ($(window).width() < 992) {
limit = 50;
}
return {
init: loop,
move: function(x,y){
// this function is called during the unraveling() function
// it moves the pinned particle and increases
// the thread length, giving the illusion of unraveling
var first = segment.particles[0];
var last = segment.particles[segment.particles.length-1];
// only increase it once every 5 loops,
// and only until 150
if ((loopIndex%5 === 0) && (segment.particles.length < 150) && first && first.lastPos) {
var pY = first.lastPos.y+2;
// make sure it doesn't drop off the bottom
// or the thread risks leaping around like a snake
if (pY > sim.height+4) {
pY = sim.height-1;
}
// add a new particle on the end
// to increase thread length
var newParticle = new Particle({
x: first.lastPos.x,
y: pY
});
segment.particles.unshift(newParticle);
// we need to modify the constrainsts between particles
var newConstraint = new DistanceConstraint( newParticle , segment.particles[1], 2);
segment.constraints.unshift(newConstraint);
}
// move pinned particle
pin.pos.x = (x/mod.x)*width;
pin.pos.y = (y/mod.y)*height;
},
end: function(){
// optional: make thread drop out the bottom
if (opts.noPooling){
sim.bounds = function(){};
}
// when finished, remove pinned particle
delete segment.constraints[segment.constraints.length-1];
delete segment.particles[segment.particles.length-1];
pin.relax();
}
};
}
/* Check if Firefox version is below 40, which has an SVG bug */
function oldFF(){
var ffversion = window.navigator.userAgent.split('Firefox/')[1];
if (ffversion && (+ffversion < 40)){
return true;
} else {
return false;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment