// // main.js // // A project template for using arbor.js // (function($){ var Renderer = function(canvas){ var canvas = $(canvas).get(0) var ctx = canvas.getContext("2d"); var particleSystem var canvasBackground; var mainNode; var balloonNode; var that = { init:function(system){ // // the particle system will call the init function once, right before the // first frame is to be drawn. it's a good place to set up the canvas and // to pass the canvas size to the particle system // // save a reference to the particle system for use in the .redraw() loop particleSystem = system // inform the system of the screen dimensions so it can map coords for us. // if the canvas is ever resized, screenSize should be called again with // the new dimensions particleSystem.screenSize(canvas.width, canvas.height) particleSystem.screenPadding(50) // leave an extra 80px of whitespace per side // set up some event handlers to allow for node-dragging that.initMouseHandling() }, redraw:function(){ // // redraw will be called repeatedly during the run whenever the node positions // change. the new positions for the nodes can be accessed by looking at the // .p attribute of a given node. however the p.x & p.y values are in the coordinates // of the particle system rather than the screen. you can either map them to // the screen yourself, or use the convenience iterators .eachNode (and .eachEdge) // which allow you to step through the actual node objects but also pass an // x,y point in the screen's coordinate system // ctx.fillStyle = "black" ctx.fillRect(0,0, canvas.width, canvas.height) ctx.imageSmoothingEnabled = true; var drawBackground = function(densityOfPoints, maxSize, minSize) { if (canvasBackground) { ctx.putImageData(canvasBackground, 0, 0); } else { minSize = minSize || 1; maxSize = maxSize || 3; densityOfPoints = densityOfPoints || 0.01; var numberOfPoints = densityOfPoints * canvas.width * canvas.height; for (var i = 0; i < numberOfPoints; i++) { var size = Math.random() * (maxSize - minSize) + minSize; var opacity = Math.random() * 0.2 + 0.4; ctx.fillStyle = 'rgba(220, 220, 255, ' + opacity + ')'; ctx.beginPath(); ctx.arc(Math.random() * canvas.width, Math.random() * canvas.height, size / 2, 0, 2 * Math.PI); ctx.fill(); } canvasBackground = ctx.getImageData(0, 0, canvas.width, canvas.height); } } drawBackground(0.02); // JESTEM BOGIEM PRZEKSZTAŁCEŃ 2D :O var drawArrow = function(pt1, pt2, length, style) { var angle = Math.atan2(pt2.y - pt1.y, pt2.x - pt1.x); ctx.fillStyle = style; ctx.setLineDash([]); ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(pt2.x, pt2.y); ctx.lineTo(pt2.x - length * Math.cos(angle - Math.PI/6), pt2.y - length * Math.sin(angle - Math.PI/6)); ctx.lineTo(pt2.x - length * (Math.cos(angle + Math.PI/6) + Math.cos(angle - Math.PI/6))/3, pt2.y - length * (Math.sin(angle - Math.PI/6) + Math.sin(angle + Math.PI/6))/3); ctx.lineTo(pt2.x - length * Math.cos(angle + Math.PI/6), pt2.y - length * Math.sin(angle + Math.PI/6)); ctx.lineTo(pt2.x, pt2.y); ctx.stroke(); ctx.fill(); } var detectDestination = function(pt1, pt2, imageBox) { var destination = {}; var intersectingSegment = undefined; if (pt1.x != pt2.x) { var lineSlope = (pt1.y - pt2.y) / (pt1.x - pt2.x); var width = imageBox.width; var height = imageBox.height; if ((-height / 2 <= lineSlope * width / 2) && (lineSlope * width / 2 <= height / 2)) { if (pt1.x > pt2.x) { intersectingSegment = [ { x: pt2.x + width / 2, y: pt2.y - height / 2 }, { x: pt2.x + width / 2, y: pt2.y + height / 2 } ]; } else { intersectingSegment = [ { x: pt2.x - width / 2, y: pt2.y - height / 2 }, { x: pt2.x - width / 2, y: pt2.y + height / 2 } ]; } } if ((-width / 2 <= (height / 2) / lineSlope) && ((height / 2) / lineSlope <= width / 2)) { if (pt1.y > pt2.y) { intersectingSegment = [ { x: pt2.x - width / 2, y: pt2.y + height / 2 }, { x: pt2.x + width / 2, y: pt2.y + height / 2 } ]; } else { intersectingSegment = [ { x: pt2.x - width / 2, y: pt2.y - height / 2 }, { x: pt2.x + width / 2, y: pt2.y - height / 2 } ]; } } } else { intersectingSegment = (pt1.y > pt2.y) ? [ { x: pt2.x - width / 2, y: pt2.y - height / 2 }, { x: pt2.x + width / 2, y: pt2.y - height / 2 } ] : [ { x: pt2.x - width / 2, y: pt2.y + height / 2 }, { x: pt2.x + width / 2, y: pt2.y + height / 2 } ]; } if (intersectingSegment) { var intersectAngle = ((intersectingSegment[1].y - intersectingSegment[0].y) * (pt2.x - pt1.x)) - ((intersectingSegment[1].x - intersectingSegment[0].x) * (pt2.y - pt1.y)); var aSlope = pt1.y - intersectingSegment[0].y; var bSlope = pt1.x - intersectingSegment[0].x; var numerator1 = ((intersectingSegment[1].x - intersectingSegment[0].x) * aSlope) - ((intersectingSegment[1].y - intersectingSegment[0].y) * bSlope); var intersectSlope = numerator1 / intersectAngle; destination = { x: pt1.x + (intersectSlope * (pt2.x - pt1.x)), y: pt1.y + (intersectSlope * (pt2.y - pt1.y)) }; } return destination; } var setOpacity = function(color, opacity) { if (color.substring(0, 5) == 'rgba(') { color = color.replace(/,[^,]*\)$/, ',' + opacity + ')'); } return color; } var drawEdge = function(edge, pt1, pt2) { // edge: {source:Node, target:Node, length:#, data:{}} // pt1: {x:#, y:#} source position in screen coords // pt2: {x:#, y:#} target position in screen coords ctx.strokeStyle = edge.data.style || "rgba(255,255,255,.333)" ctx.lineWidth = edge.data.width || 3 ctx.setLineDash(edge.data.dash || []) // draw a line from pt1 to pt2 ctx.beginPath() ctx.moveTo(pt1.x, pt1.y) if (edge.data.arrow) { if (edge.target.data.imageObject) { var destination = detectDestination(pt1, pt2, edge.target.data.imageObject); if (destination) { pt2 = destination; } } } if (mainNode && balloonNode) { if (edge.source.data.balloon || edge.target.data.balloon || (balloonNode != mainNode && particleSystem.getEdgesTo(balloonNode)[0].source == edge.target && edge.source == mainNode)) { ctx.strokeStyle = setOpacity(edge.target.data.color, 0.8); } } ctx.lineTo(pt2.x, pt2.y); ctx.stroke(); if (edge.data.arrow) { drawArrow(pt1, pt2, edge.data.arrow, ctx.strokeStyle); } }; var drawImage = function(image, coords, color, strokeColor) { if (!image.height) { image.height = image.width ? image.width * image.naturalHeight / image.naturalWidth : image.naturalHeight; } if (!image.width) { image.width = image.height ? image.height * image.naturalWidth / image.naturalHeight : image.naturalWidth; } ctx.save(); var color = color || 'black'; ctx.fillStyle = color; ctx.lineWidth = 5; ctx.beginPath(); ctx.arc(coords.x, coords.y, image.width / 2, 0, Math.PI * 2); ctx.clip(); ctx.drawImage(image, coords.x - image.width / 2, coords.y - image.height / 2, image.width, image.height); ctx.fill(); ctx.restore(); ctx.lineWidth = 3; ctx.strokeStyle = strokeColor; ctx.beginPath(); ctx.arc(coords.x, coords.y, image.width / 2, 0, Math.PI * 2); ctx.stroke(); }; var repositionBalloon = function(div, node) { var topOffset = node.data.p.y - node.data.height / 2; if (topOffset < 0) { topOffset = 0; } if (topOffset + div.outerHeight() > $(window).height()) { topOffset = $(window).height() - div.outerHeight(); } var leftMargin = node.data.width / 2 + 2; if (mainNode && node.data.p.x < mainNode.data.p.x) { leftMargin = -leftMargin - div.outerWidth(); } var leftOffset = node.data.p.x + leftMargin; if (leftOffset < 0) { leftOffset += div.outerWidth() + node.data.width + 4; } if (leftOffset + div.outerWidth() > $(window).width()) { leftOffset -= div.outerWidth() + node.data.width + 4; } div.css('top', topOffset); div.css('left', leftOffset); div.show(); } var currentZIndex = 0; var drawNode = function(node, pt){ // node: {mass:#, p:{x,y}, name:"", data:{}} // pt: {x:#, y:#} node position in screen coords node.data.p = pt; node.data.zindex = currentZIndex++; if (node.data.main) { mainNode = node; if (history.replaceState) { history.replaceState(null, null, '#' + node.name); } } if (node.data.image && node.data.width > 10) { var isHighlighted = function(node) { if (node.data.balloon) { return true; } if (balloonNode && mainNode) { if (node == mainNode) { return true; } var targets = particleSystem.getEdgesFrom(node); for (var t = 0; t < targets.length; t++) { if (targets[t].target == balloonNode) { return true; } } } } var strokeColor = isHighlighted(node) ? setOpacity(node.data.color, 0.8) : 'rgba(255, 255, 255, 0.7)'; if (node.data.imageObject) { node.data.imageObject.width = node.data.width; node.data.imageObject.height = node.data.height; drawImage(node.data.imageObject, pt, node.data.color, strokeColor); } else { var img = new Image(node.data.width, node.data.height); img.onload = function() { node.data.imageObject = img; drawImage(node.data.imageObject, pt, node.data.color, strokeColor); }; img.src = '_img/' + node.data.image; } } else { var color = node.data.color || 'black'; ctx.fillStyle = color; ctx.lineWidth = 5; ctx.strokeStyle = 'rgba(255,255,255,0.33)'; ctx.beginPath(); ctx.arc(pt.x, pt.y, node.data.width / 2, 0, Math.PI*2); ctx.stroke(); ctx.fill(); } if (node.data.balloon) { repositionBalloon(node.data.balloon, node); if (node.data.oldWidth) { node.data.width = node.data.oldWidth; } if (node.data.oldHeight) { node.data.height = node.data.oldHeight; } balloonNode = node; } }; particleSystem.eachEdge(drawEdge); particleSystem.eachNode(drawNode); if (balloonNode) { var selectedDimension = Math.max((balloonNode.data.image) ? 100 : 50, balloonNode.data.width); balloonNode.data.oldWidth = balloonNode.data.width; balloonNode.data.width = selectedDimension; balloonNode.data.oldHeight = balloonNode.data.height; balloonNode.data.height = selectedDimension; drawNode(balloonNode, balloonNode.data.p); } }, initMouseHandling:function(){ var hideBalloon = function() { if (balloon) { balloon.data('node').data.balloon = undefined; balloon.remove(); balloon = undefined; balloonNode = undefined; } } var fixedNodes = 0; var moveNode = function(node, newNode, duration) { var steps = 20; duration = duration || 1000; var xStep = (newNode.x - node.p.x) / steps; var yStep = (newNode.y - node.p.y) / steps; var originalPosition = { x: node.p.x, y: node.p.y }; for (var step = 0; step < steps; step++) { setTimeout( function(i) { return function() { node.p = { x: originalPosition.x + xStep * i, y: originalPosition.y + yStep * i }; if (i == steps - 1) { fixedNodes--; } } }(step), step * duration / steps ); } } var switchContext = function() { var link = $(this); hideBalloon(); $.getJSON( '_data/' + link.attr('data-link') + '.json?' + (new Date()).getTime(), function(newGraph) { fixedNodes = 0; particleSystem.prune(function(node) { if (node.data.main) { node.fixed = false; } if (newGraph.nodes[node.name]) { node.fixed = true; node.data.mass = node.mass; node.tempMass = 10000; fixedNodes++; moveNode(node, newGraph.nodes[node.name], 1500); return false; } else { node.tempMass = 0.001; return true; } }); var mergeInterval = setInterval(function() { if (fixedNodes == 0) { particleSystem.merge(newGraph); particleSystem.prune(function(node) { node.tempMass = node.mass; return false; }); clearInterval(mergeInterval); } }, 100); } ); } var balloon; // start listening $(canvas).click(function(e) { hideBalloon(); var pos = $(canvas).offset(); _mouseP = arbor.Point(e.pageX-pos.left, e.pageY-pos.top) var candidateNode = undefined; particleSystem.eachNode(function(node) { var distance = _mouseP.subtract(particleSystem.toScreen(node.p)); if (Math.sqrt(distance.x*distance.x + distance.y*distance.y) <= node.data.width / 2 + 3) { if (!candidateNode || candidateNode.data.zindex < node.data.zindex) { candidateNode = node; } } }); if (candidateNode) { balloon = $('
'); balloon.css({ 'position': 'absolute', 'background': 'rgba(0,0,0,0.45)', 'color': 'red', 'max-width': '150px', 'max-height': '150px', 'overflow': 'hidden', 'padding': '5px', 'border': 'solid 2px white', 'border-radius': '5px' }); balloon.append($('

').text(candidateNode.data.name)); if (!candidateNode.data.main) { var switchLink = $('').attr({ 'href': '#', 'data-link': candidateNode.name }); switchLink.text('[go!]'); switchLink.click(switchContext); balloon.append(switchLink); } balloon.hide(); $('body').append(balloon); balloon.data('node', candidateNode); candidateNode.data.balloon = balloon; } }); }, } return that } var sys; $(document).ready(function() { var graphID = 'graph'; if (location.hash) { graphID = location.hash.replace(/^#/, ''); } $.getJSON('_data/' + graphID + '.json', function(graph) { graph.repulsion = graph.repulsion || 20; graph.stiffness = graph.stiffness || 1; graph.friction = graph.friction || 0.99; graph.gravity = graph.gravity || true; graph.fps = graph.fps || 10; graph.precision = graph.precision || 0.1; graph.dt = graph.dt || 0.11; sys = arbor.ParticleSystem(graph.repulsion, graph.stiffness, graph.friction, graph.gravity, graph.fps, graph.dt, graph.precision); sys.renderer = Renderer("#viewport"); // our newly created renderer will have its .init() method called shortly by sys... $(window).trigger('resize'); sys.graft(graph); }); $(window).resize(function() { var width = $(window).width() - 5; var height = $(window).height() - 5; $('#viewport').attr({'width': width, 'height': height}); sys.screenSize(width, height); }); }) })(this.jQuery)