/* d3 gangnam style! * simplifed test case * moving force-directed graph nodes via transform * coded by Ken Penn */ (function () { window.reqAniFrame = function(win, t) { return win["r" + t] || win["webkitR" + t] || win["mozR" + t] || win["msR" + t] || function(fn) { setTimeout(fn, 60) } } (window, "equestAnimationFrame"); var gs = { svgBox : d3.select('.psy-svg-box'), svg : d3.select('.psy-svg-box svg'), height : 0, ctrY : 0, baseY : 806, // maximized viewport height on my 15" macbook adjY : 0, width : 0, ctrX : 0, baseX : 1392, // maximized viewport width on my 15" macbook adjX : 0, smallest : 0, nodes : '', links : '', figs : [], fignewt : 0, trace : true, // bpm : 60000 / 64, bpm : 60000 / 128, // 128 bpm, ~469 ms init : function () { var fig = {}; // create a stick figure, start the graph fig = gs.crtGrp(gs.figs.length); setTimeout(function () { gs.standUp(fig); }, gs.bpm * 4); setTimeout(function () { gs.akimbo(fig); }, gs.bpm * 8); setTimeout(function () { gs.gandy(fig); gs.jive(fig); //gs.slideGrp(fig, 16); }, gs.bpm * 12); setTimeout(function () { gs.wave(fig); }, gs.bpm * 24); }, setDims : function () { var part, dims = gs.svgBox.node().getBoundingClientRect(); // set the dimensions for the svg element gs.height = dims.height; gs.ctrY = gs.height / 2; gs.width = dims.width; gs.ctrX = gs.width / 2; gs.smallest = gs.height < gs.width ? gs.height : gs.width; gs.svg = gs.svgBox.select('svg') .attr('height', gs.height) .attr('width', gs.width); // scale the body parts gs.adjX = gs.width / gs.baseX; gs.adjY = gs.height / gs.baseY; gs.adjust = gs.smallest === gs.height ? gs.adjY : gs.adjX; for (part in gs.parts) { if (gs.parts.hasOwnProperty(part)) { if (gs.parts[part].ld) { gs.parts[part].ld = gs.adjust * gs.parts[part].ld > 4 ? Math.round(gs.adjust * gs.parts[part].ld) : 4; } } } recurse(gs.bod); function recurse (part) { if (part.reqX) { part.reqX = Math.round(part.reqX * gs.adjust); } if (part.reqY) { part.reqY = Math.round(part.reqY * gs.adjust); } if (part.children) { part.children.forEach(recurse); } } }, crtGrp : function (ct) { var fig = gs.svg.append('g') .classed('fig-' + ct, true); fig.nodes = ''; fig.links = ''; // init the force layout fig.force = d3.layout.force() .on('tick', function (d) { gs.tick(fig); }) .size([gs.width, gs.height]); gs.update(fig); gs.classLine(fig); gs.figs.push(fig); return fig; }, classLine : function (fig) { fig.links.each(function (d) { d3.select(this).classed('src-' + d.source.name, true) .classed('trg-' + d.target.name, true); }); }, update : function (fig) { var root = gs.clone(gs.bod), fit = 0, charge = 0, gravity = 0; fig.nodes = gs.flatten(root), fig.links = d3.layout.tree().links(fig.nodes), fit = Math.sqrt(fig.nodes.length / (gs.smallest * gs.smallest)), charge = ( -1 / fit ) * .5 * gs.adjust, gravity = ( 5 * fit ); fig.selectAll('line.link').remove(); fig.selectAll('circle.node').remove(); // start the force layout fig.force .charge(charge) .linkDistance( function (d) { return gs.parts[d.target.part].ld; }) .gravity(gravity) .nodes(fig.nodes) .links(fig.links) .start(); // Update the links… fig.links = fig.selectAll('line.link') .data(fig.links, function(d) { return d.target.id; }); // Enter any new links. fig.links.enter().insert('line', '.node') .attr({ 'class' : 'link', x1 : function(d) { return d.source.x; }, y1 : function(d) { return d.source.y; }, x2 : function(d) { return d.target.x; }, y2 : function(d) { return d.target.y; } }); // Exit any old links. fig.links.exit().remove(); // Update the nodes… fig.nodes = fig.selectAll('circle.node') .data(fig.nodes, function(d) { return d.id; }) // Enter any new nodes fig.nodes.enter() .append('circle') .attr('class', function (d) { var outer = d.outer? ' outer' : ''; return 'node ' + d.name + outer; }) .attr('transform', function(d) { return 'translate(' + gs.to3(d.x) + ',' + gs.to3(d.y) + ')'; }) .attr('r', function (d) { var r, adj; adj = gs.adjX < gs.adjY ? gs.adjX : gs.adjY; r = Math.ceil(gs.parts[d.part].r * adj); r = r > 4 ? r : 4; return r; }) .call(fig.force.drag) // Exit any old nodes. fig.nodes.exit().remove(); }, tick : function (fig) { fig.nodes.attr('transform', function(d) { return 'translate(' + gs.to3(d.x) + ',' + gs.to3(d.y) + ')'; }); fig.links.attr('x1', function(d) { return d.source.x; }) .attr('y1', function(d) { return d.source.y; }) .attr('x2', function(d) { return d.target.x; }) .attr('y2', function(d) { return d.target.y; }); }, standUp : function (fig) { var cb = function (d) { if ( d.outer && d.name !== 'head' || d.name === 'bod') { return d.fixed = true; } }, ctrX = fig.ctrX || gs.ctrX, ctrY = fig.ctrY || gs.ctrY; fig.lines = fig.selectAll('line.link'); fig.nodes.each(function (d, i) { var mov = { d : d, el : this, endX : ctrX + d.reqX, endY : ctrY + d.reqY, fig : fig }; fig.force.stop(); mov = gs.buildMove(mov); gs.transNode(mov, cb); }); }, akimbo : function (fig) { var hands = fig.selectAll('.node.lHand, .node.rHand'); fig.selectAll('circle.lElbow, circle.rElbow') .each(function (d) { return d.fixed = true; }) hands.each(function (d) { var mov = { d : d, el : this, fig : fig }; if (d.name === 'lHand') { mov.endX = gs.to3(gs.ctrX + (gs.adjust * 33)); mov.endY = gs.to3(gs.ctrY + (gs.adjY * -48)); } else { mov.endX = gs.to3(gs.ctrX + (gs.adjust * -33)); mov.endY = gs.to3(gs.ctrY + (gs.adjY * -48)); } mov = gs.buildMove(mov); gs.transNode(mov); }); }, jive : function (fig) { var hands = fig.selectAll('.node.lHand, .node.rHand'), lbows = fig.selectAll('.node.lElbow, .node.rElbow'), radX = gs.adjust * 5, radY = gs.adjust * 25; hands.each(function (d) { jivin(d, this); }); function elbows () { lbows.each(function (d) { jivin(d, this); }); } function jivin (d, el) { var mov = { d : d, el : el, pc : 'jive-', fig: fig }; mov = gs.buildMove(mov); if (d.name === ('lHand' || 'rElbow') ) { mov.endX = mov.begX - radX; } else { mov.endX = mov.begX + radX; } mov.endY = gs.to3(mov.begY - (gs.adjust * 10)); mov = gs.buildMove(mov); mov.pc = mov.pc + d.name; crtPath(mov); gs.transNode(mov, function (d) { gs.ptAlongPath({ path : fig.select('path.' + mov.pc), circle : d3.select(mov.el), lines : mov.lines, count : mov.d.part === 'hand' ? 8 : 7, fig : mov.fig }); if (mov.d.name === 'lHand') { elbows(); } }); } function crtPath (mov) { var rad; if ( mov.d.name === ('rHand' || 'rElbow' ) ) { rad = -radX; } else { rad = radX; } mov.fig.append('path') .attr('d', 'M ' + mov.endX + ',' + mov.endY + ' a ' + rad + ',' + radY + ' 0 0,0 ' + -rad + ',' + (gs.adjY * -8) + ' a ' + rad + ',' + radY + ' 0 1,0 ' + rad + ',' + (gs.adjY * 8) + ' z' ) .attr('class', mov.pc) .attr('stroke', 'none') .attr('fill', 'none') } }, gandy : function(fig) { var hoppers = fig.selectAll('.node.lFoot, .node.lKnee, .node.rFoot, .node.rKnee'); hoppers.each(function(d) { var hopper = gs.getRectCtr(this); var radX = gs.to3(60 * gs.adjust); var radY = gs.to3(8 * radX); var dir = d.name === ('lFoot' || 'lKnee') ? 1 : -1; var sweep = d.name === ('lFoot' || 'lKnee') ? [1,0] : [0,1]; var stroke = d.name === 'lFoot' ? 'limegreen' : 'magenta'; var dpath; if (d.part === 'knee') { radX *= 0.5; radY *= 0.5; } dpath = 'M' + hopper.x + ',' + hopper.y + ' a' + radX + ',' + radY + ' 0 0,' + sweep[0] + ' ' + (dir * radX) + ',0 v1' + ' a' + radX + ',' + radY + ' 0 0,' + sweep[1] + ' ' + (-dir * radX) + ',0 v-1 z'; fig.append('path') .classed(d.name + '-hop', true) .attr('d', dpath) .attr('stroke', 'none') .attr('fill', 'none') }); hoppers.each(function (d) { var hopper = gs.getRectCtr(this); var mov = { d : d, endX : hopper.x, endY : hopper.y, el : this, fig : fig }; mov = gs.buildMove(mov); gs.transNode(mov, function (d) { if ( d.name === 'rFoot' || d.name === 'rKnee' ) { hop(); } else { setTimeout(function () { hop(); }, gs.bpm); } function hop() { gs.ptAlongPath({ path : fig.select('path.' + d.name + '-hop'), circle : d3.select(mov.el), lines : mov.lines, count : 8, fig : fig, dur : gs.bpm * 2 }); } }); }); }, wave : function (fig) { var fist = fig.select('circle.lHand'); var strX = gs.ctrX - (gs.adjust * 80); var strY = gs.ctrY - (gs.adjust * 220); var radX = gs.adjust * 65; var radY = gs.adjust * 25; var cb = function (d) { return d.fixed = true; }; fig.append('path') .attr('d', 'M ' + strX + ',' + strY + ' a ' + radX + ',' + radY + ' 10 0,0 ' + -radX + ',' + (gs.adjY * -8) + ' a ' + radX + ',' + radY + ' 10 1,0 ' + radX + ',' + (gs.adjY * 8) + ' z' ) .attr('class', 'wave') .attr('stroke', 'none') .attr('fill', 'none') fist.each(function (d) { var mov = { d : d, endX : strX, endY : strY, el : this, pc : 'wave', fig : fig }; mov = gs.buildMove(mov); gs.transNode(mov, function (d) { gs.ptAlongPath({ path : fig.select('path.' + mov.pc), circle : d3.select(mov.el), lines : mov.lines, count : 7, fig : fig }); }); }); elbows(); function elbows() { var lBows = fig.selectAll('.node.lElbow, .node.rElbow'); lBows.each(function (d) { var mov = { el : this, d : d, dur : gs.bpm * 2, fig : fig }; if (d.name === 'lElbow') { mov.endX = gs.ctrX - (111 * gs.adjust); mov.endY = gs.ctrY - (130 * gs.adjust); } else { mov.endX = gs.ctrX + (48 * gs.adjust); mov.endY = gs.ctrY - (57 * gs.adjust); } mov = gs.buildMove(mov); gs.crtLine(mov); gs.transNode(mov); }); } }, crtLine : function(mov) { console.dir(mov) mov.fig.append('path') .attr('d', 'M ' + mov.begX + ',' + mov.begY + 'L ' + mov.endX + ',' + mov.endY ) .attr('fill', 'none') .attr('stroke', 'none') }, buildMove : function (mov) { var begXY = gs.getTransXY(mov.el); mov.begX = begXY.x; mov.begY = begXY.y; mov.lines = { source : mov.fig.selectAll('line.src-' + mov.d.name), target : mov.fig.selectAll('line.trg-' + mov.d.name), } return mov; }, transNode : function (mov, callback) { var dur = mov.dur || gs.bpm * 2; d3.select(mov.el) .transition() .duration(dur) .attr('transform', function(d) { return 'translate(' + mov.endX + ',' + mov.endY + ')'; }) .tween('tweenLine', function (d) { return function (t) { gs.tweenLine(t, mov) }}) .each('end', function (d) { d.x = mov.endX; d.y = mov.endY; d.px = mov.endX; d.py = mov.endY; mov.fig.force.resume(); if (callback) { callback(d); } }); }, slideGrp : function ( grp, ct, x, arr ) { var idx = x || 0; var slides = arr || [ { x: 100, y: 0 }, { x: 0, y: 0 }, { x: -100, y: 0 }, { x: 0, y: 0 }/*, { x: 200, y: 0 }, { x: 0, y: 0 }, { x: -200, y: 0 }, { x: 0, y: 0 }*/ ]; var len = slides.length; grp.transition() .duration(gs.bpm * 2) .attr('transform', function(d) { return 'translate(' + slides[idx].x + ',' + slides[idx].y + ')'; }) .each('end', function (d) { ct -= 1; if (!ct) {return;} idx = idx === len - 1 ? 0 : idx + 1; reqAniFrame(function () { gs.slideGrp ( grp, ct, idx, slides); }); }); }, tweenLine : function (t, mov) { var cr = gs.getRectCtr(mov.el); var trc = { el : mov.el.parentElement, cx : cr.x, cy : cr.y, rad : 3 }; mov.fig.force.stop(); // manipulating the line(s) the node is attached to mov.lines.source.each(function (d) { d.source.x = cr.x; d.source.y = cr.y; d3.select(this).attr('x1', cr.x) .attr('y1', cr.y); }); mov.lines.target.each(function (d) { d.target.x = cr.x; d.target.y = cr.y; d.target.px = cr.x; d.target.py = cr.y; d3.select(this).attr('x2', cr.x) .attr('y2', cr.y); }); // mov.fig.force.resume(); if ( gs.trace ) { gs.tracers(trc) } }, ptAlongPath : function(m) { m.circle.transition() .duration(m.dur || gs.bpm) .attrTween('transform', gs.transAlong(m)) .tween('lines', gs.ptLines(m)) .each('end', function () { m.fig.force.resume(); if (m.count) { m.count -= 1; reqAniFrame(function () { gs.ptAlongPath(m); }); } }); }, ptLines : function(m) { var path = m.path.node(), len = path.getTotalLength(); return function(d, i, a) { return function(t) { var p = path.getPointAtLength(t * len); // manipulating the line(s) the node is attached to m.lines.source.each(function (d) { d3.select(this).attr('x1', p.x) .attr('y1', p.y); }); m.lines.target.each(function (d) { d3.select(this).attr('x2', p.x) .attr('y2', p.y); }); }; }; }, transAlong : function(m) { var path = m.path.node(), len = path.getTotalLength(); return function(d, i, a) { return function(t) { var p = path.getPointAtLength(t * len), trc = { el : path.parentNode, cx : p.x, cy : p.y }; if ( gs.trace ) { gs.tracers(trc) } return 'translate(' + p.x + ',' + p.y + ')'; }; }; }, getTransXY : function (el) { var s = d3.select(el).attr('transform'), t = []; s = s.substring(s.indexOf('translate(')); s = s.substring(0, s.indexOf(')')); t = s.replace('translate(','') .replace(')','') .split(','); return { x : parseFloat(t[0]), y : parseFloat(t[1]) }; }, getRectCtr : function (el) { var cr = el.getBoundingClientRect(); return { x : gs.to3((cr.left + cr.right) * 0.5), y : gs.to3((cr.top + cr.bottom) * 0.5) }; }, tracers : function (trc) { setTimeout( function () { var d3el = d3.select(trc.el), rad = trc.rad || 2, dur = trc.dur || gs.bpm, fill = trc.fill || 'magenta'; d3el.append('circle') .attr('r', rad) .attr('cx', trc.cx) .attr('cy', trc.cy) .attr('fill', fill ) .attr('stroke', 'none') .transition() .duration(dur) .delay(300) .attr('opacity', 0) .remove(); }, 200) }, tweenChk : function (t, mov, d) { var cr = gs.getRectCtr(mov.el), cx = cr.x, cy = cr.y, pel = mov.el.parentElement, trc = { el : pel, cx : cx, cy : cy, dur : gs.bpm * 2 }; gs.tracers(trc); }, showXY : function () { d3.select('body').on('mousemove', function() { console.log('page X: ' + d3.event.pageX +'\npage Y: ' + d3.event.pageY) }); }, showPos : function (fig) { var adjustX = parseFloat(gs.to3(gs.baseX / gs.width)), adjustY = parseFloat(gs.to3(gs.baseY / gs.height)); console.log('adjust x: ' + adjustX); console.log('adjust y: ' + adjustY); fig.nodes.each(function (d, i) { var str = '\n' + d.name + '\nadjust x: ' + Math.round((d.x - gs.ctrX) * adjustX) + ' actual x: ' + Math.round(d.x - gs.ctrX) + '\nadjust y: ' + Math.round((d.y - gs.ctrY) * adjustY) + ' actual y: ' + Math.round(d.y - gs.ctrY); console.log(str); }); }, flatten : function (fig) { var nodes = [], i = 0; function recurse (node) { if (node.children) { node.children.forEach(recurse); } else { node.outer = true; } if (!node.id) { node.id = ++ i; } nodes.push(node); } recurse(fig); return nodes; }, parts : { bod : { ld : 10 }, head : { ld : 50, r : 24 }, arm : { ld : 40 }, elbow : { ld : 50 }, hand : { ld : 50, r : 12 }, hips : { ld : 70 }, leg : { ld : 10 }, knee : { ld : 60 }, foot : { ld : 100, r : 15 } }, bod : { name : 'bod', part : 'bod', reqX : 0, reqY : -125, children : [ { name : 'hips', part : 'hips', reqX : 0, reqY : -26, children : [ { name : 'lLeg', part : 'leg', reqX : -17, reqY : 3, children : [ { name : 'lKnee', part: 'knee', reqX : -46, reqY : 72, children : [ { name : 'lFoot', part : 'foot', reqX : -37, reqY : 180 } ] } ] }, { name : 'rLeg', part : 'leg', reqX : 20, reqY : 7, children : [ { name : 'rKnee', part: 'knee', reqX : 46, reqY : 72, children : [ { name : 'rFoot', part : 'foot', reqX : 37, reqY : 180 } ] } ] } ] }, { name : 'head', part : 'head', reqX : 0, reqY : -180 }, { name : 'lArm', part : 'arm', reqX : -45, reqY : -120, children : [ { name : 'lElbow', part: 'elbow', reqX : -70, reqY : -60, children : [ { name : 'lHand', part : 'hand', reqX : -45, reqY : 11 } ] } ] }, { name : 'rArm', part : 'arm', reqX : 45, reqY : -120, children : [ { name : 'rElbow', part: 'elbow', reqX : 70, reqY : -60, children : [ { name : 'rHand', part : 'hand', reqX : 45, reqY : 11 } ] } ] } ] }, clone : function (src) { // courtesy of David Walsh function mixin (dest, source, copyFunc) { var name, s, i, empty = {}; for(name in source) { // the (!(name in empty) || empty[name] !== s) condition avoids copying properties in 'source' // inherited from Object.prototype. For example, if dest has a custom toString() method, // don't overwrite it with the toString() method that source inherited from Object.prototype s = source[name]; if(!(name in dest) || (dest[name] !== s && (!(name in empty) || empty[name] !== s))) { dest[name] = copyFunc ? copyFunc(s) : s; } } return dest; } if(!src || typeof src != 'object' || Object.prototype.toString.call(src) === '[object Function]') { // null, undefined, any non-object, or function return src; // anything } if(src.nodeType && 'cloneNode' in src) { // DOM Node return src.cloneNode(true); // Node } if(src instanceof Date) { // Date return new Date(src.getTime()); // Date } if(src instanceof RegExp) { // RegExp return new RegExp(src); // RegExp } var r, i, l; if(src instanceof Array) { // array r = []; i = 0; l = src.length; for(i; i < l; ++i) { if(i in src) { r.push(gs.clone(src[i])); } } // we don't clone functions for performance reasons // }else if(d.isFunction(src)) { // // function // r = function() { return src.apply(this, arguments); }; } else { // generic objects r = src.constructor ? new src.constructor() : {}; } return mixin(r, src, gs.clone); }, //end clone to3 : function (n) { return parseFloat(n.toFixed(3)) } }; gs.setDims(); gs.init(); if (window.gs) { window.gangnamStyle = gs; } else { window.gs = gs; } }());