delete this page
-
diff --git a/index.html b/index.html
index 94212b2..f00b84d 100644
--- a/index.html
+++ b/index.html
@@ -68,12 +68,17 @@
}
}
+
+
handle
+
+
+
+
+
\ No newline at end of file
diff --git a/springy.js b/springy.js
new file mode 100644
index 0000000..0bf5ba4
--- /dev/null
+++ b/springy.js
@@ -0,0 +1,735 @@
+/**
+ * Springy v2.7.1
+ *
+ * Copyright (c) 2010-2013 Dennis Hotson
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ */
+(function (root, factory) {
+ if (typeof define === 'function' && define.amd) {
+ // AMD. Register as an anonymous module.
+ define(function () {
+ return (root.returnExportsGlobal = factory());
+ });
+ } else if (typeof exports === 'object') {
+ // Node. Does not work with strict CommonJS, but
+ // only CommonJS-like enviroments that support module.exports,
+ // like Node.
+ module.exports = factory();
+ } else {
+ // Browser globals
+ root.Springy = factory();
+ }
+}(this, function() {
+
+ var Springy = {};
+
+ var Graph = Springy.Graph = function() {
+ this.nodeSet = {};
+ this.nodes = [];
+ this.edges = [];
+ this.adjacency = {};
+
+ this.nextNodeId = 0;
+ this.nextEdgeId = 0;
+ this.eventListeners = [];
+ };
+
+ var Node = Springy.Node = function(id, data) {
+ this.id = id;
+ this.data = (data !== undefined) ? data : {};
+
+ // Data fields used by layout algorithm in this file:
+ // this.data.mass
+ // Data used by default renderer in springyui.js
+ // this.data.label
+ };
+
+ var Edge = Springy.Edge = function(id, source, target, data) {
+ this.id = id;
+ this.source = source;
+ this.target = target;
+ this.data = (data !== undefined) ? data : {};
+
+ // Edge data field used by layout alorithm
+ // this.data.length
+ // this.data.type
+ };
+
+ Graph.prototype.addNode = function(node) {
+ if (!(node.id in this.nodeSet)) {
+ this.nodes.push(node);
+ }
+
+ this.nodeSet[node.id] = node;
+
+ this.notify();
+ return node;
+ };
+
+ Graph.prototype.addNodes = function() {
+ // accepts variable number of arguments, where each argument
+ // is a string that becomes both node identifier and label
+ for (var i = 0; i < arguments.length; i++) {
+ var name = arguments[i];
+ var node = new Node(name, {label:name});
+ this.addNode(node);
+ }
+ };
+
+ Graph.prototype.addEdge = function(edge) {
+ var exists = false;
+ this.edges.forEach(function(e) {
+ if (edge.id === e.id) { exists = true; }
+ });
+
+ if (!exists) {
+ this.edges.push(edge);
+ }
+
+ if (!(edge.source.id in this.adjacency)) {
+ this.adjacency[edge.source.id] = {};
+ }
+ if (!(edge.target.id in this.adjacency[edge.source.id])) {
+ this.adjacency[edge.source.id][edge.target.id] = [];
+ }
+
+ exists = false;
+ this.adjacency[edge.source.id][edge.target.id].forEach(function(e) {
+ if (edge.id === e.id) { exists = true; }
+ });
+
+ if (!exists) {
+ this.adjacency[edge.source.id][edge.target.id].push(edge);
+ }
+
+ this.notify();
+ return edge;
+ };
+
+ Graph.prototype.addEdges = function() {
+ // accepts variable number of arguments, where each argument
+ // is a triple [nodeid1, nodeid2, attributes]
+ for (var i = 0; i < arguments.length; i++) {
+ var e = arguments[i];
+ var node1 = this.nodeSet[e[0]];
+ if (node1 == undefined) {
+ throw new TypeError("invalid node name: " + e[0]);
+ }
+ var node2 = this.nodeSet[e[1]];
+ if (node2 == undefined) {
+ throw new TypeError("invalid node name: " + e[1]);
+ }
+ var attr = e[2];
+
+ this.newEdge(node1, node2, attr);
+ }
+ };
+
+ Graph.prototype.newNode = function(data) {
+ var node = new Node(this.nextNodeId++, data);
+ this.addNode(node);
+ return node;
+ };
+
+ Graph.prototype.newEdge = function(source, target, data) {
+ var edge = new Edge(this.nextEdgeId++, source, target, data);
+ this.addEdge(edge);
+ return edge;
+ };
+
+
+ // add nodes and edges from JSON object
+ Graph.prototype.loadJSON = function(json) {
+ /**
+ Springy's simple JSON format for graphs.
+
+ historically, Springy uses separate lists
+ of nodes and edges:
+
+ {
+ "nodes": [
+ "center",
+ "left",
+ "right",
+ "up",
+ "satellite"
+ ],
+ "edges": [
+ ["center", "left"],
+ ["center", "right"],
+ ["center", "up"]
+ ]
+ }
+
+ **/
+ // parse if a string is passed (EC5+ browsers)
+ if (typeof json == 'string' || json instanceof String) {
+ json = JSON.parse( json );
+ }
+
+ if ('nodes' in json || 'edges' in json) {
+ this.addNodes.apply(this, json['nodes']);
+ this.addEdges.apply(this, json['edges']);
+ }
+ }
+
+
+ // find the edges from node1 to node2
+ Graph.prototype.getEdges = function(node1, node2) {
+ if (node1.id in this.adjacency
+ && node2.id in this.adjacency[node1.id]) {
+ return this.adjacency[node1.id][node2.id];
+ }
+
+ return [];
+ };
+
+ // remove a node and it's associated edges from the graph
+ Graph.prototype.removeNode = function(node) {
+ if (node.id in this.nodeSet) {
+ delete this.nodeSet[node.id];
+ }
+
+ for (var i = this.nodes.length - 1; i >= 0; i--) {
+ if (this.nodes[i].id === node.id) {
+ this.nodes.splice(i, 1);
+ }
+ }
+
+ this.detachNode(node);
+ };
+
+ // removes edges associated with a given node
+ Graph.prototype.detachNode = function(node) {
+ var tmpEdges = this.edges.slice();
+ tmpEdges.forEach(function(e) {
+ if (e.source.id === node.id || e.target.id === node.id) {
+ this.removeEdge(e);
+ }
+ }, this);
+
+ this.notify();
+ };
+
+ // remove a node and it's associated edges from the graph
+ Graph.prototype.removeEdge = function(edge) {
+ for (var i = this.edges.length - 1; i >= 0; i--) {
+ if (this.edges[i].id === edge.id) {
+ this.edges.splice(i, 1);
+ }
+ }
+
+ for (var x in this.adjacency) {
+ for (var y in this.adjacency[x]) {
+ var edges = this.adjacency[x][y];
+
+ for (var j=edges.length - 1; j>=0; j--) {
+ if (this.adjacency[x][y][j].id === edge.id) {
+ this.adjacency[x][y].splice(j, 1);
+ }
+ }
+
+ // Clean up empty edge arrays
+ if (this.adjacency[x][y].length == 0) {
+ delete this.adjacency[x][y];
+ }
+ }
+
+ // Clean up empty objects
+ if (isEmpty(this.adjacency[x])) {
+ delete this.adjacency[x];
+ }
+ }
+
+ this.notify();
+ };
+
+ /* Merge a list of nodes and edges into the current graph. eg.
+ var o = {
+ nodes: [
+ {id: 123, data: {type: 'user', userid: 123, displayname: 'aaa'}},
+ {id: 234, data: {type: 'user', userid: 234, displayname: 'bbb'}}
+ ],
+ edges: [
+ {from: 0, to: 1, type: 'submitted_design', directed: true, data: {weight: }}
+ ]
+ }
+ */
+ Graph.prototype.merge = function(data) {
+ var nodes = [];
+ data.nodes.forEach(function(n) {
+ nodes.push(this.addNode(new Node(n.id, n.data)));
+ }, this);
+
+ data.edges.forEach(function(e) {
+ var from = nodes[e.from];
+ var to = nodes[e.to];
+
+ var id = (e.directed)
+ ? (id = e.type + "-" + from.id + "-" + to.id)
+ : (from.id < to.id) // normalise id for non-directed edges
+ ? e.type + "-" + from.id + "-" + to.id
+ : e.type + "-" + to.id + "-" + from.id;
+
+ var edge = this.addEdge(new Edge(id, from, to, e.data));
+ edge.data.type = e.type;
+ }, this);
+ };
+
+ Graph.prototype.filterNodes = function(fn) {
+ var tmpNodes = this.nodes.slice();
+ tmpNodes.forEach(function(n) {
+ if (!fn(n)) {
+ this.removeNode(n);
+ }
+ }, this);
+ };
+
+ Graph.prototype.filterEdges = function(fn) {
+ var tmpEdges = this.edges.slice();
+ tmpEdges.forEach(function(e) {
+ if (!fn(e)) {
+ this.removeEdge(e);
+ }
+ }, this);
+ };
+
+
+ Graph.prototype.addGraphListener = function(obj) {
+ this.eventListeners.push(obj);
+ };
+
+ Graph.prototype.notify = function() {
+ this.eventListeners.forEach(function(obj){
+ obj.graphChanged();
+ });
+ };
+
+ // -----------
+ var Layout = Springy.Layout = {};
+ Layout.ForceDirected = function(graph, stiffness, repulsion, damping, minEnergyThreshold, maxSpeed) {
+ this.graph = graph;
+ this.stiffness = stiffness; // spring stiffness constant
+ this.repulsion = repulsion; // repulsion constant
+ this.damping = damping; // velocity damping factor
+ this.minEnergyThreshold = minEnergyThreshold || 0.01; //threshold used to determine render stop
+ this.maxSpeed = maxSpeed || Infinity; // nodes aren't allowed to exceed this speed
+
+ this.nodePoints = {}; // keep track of points associated with nodes
+ this.edgeSprings = {}; // keep track of springs associated with edges
+ };
+
+ Layout.ForceDirected.prototype.point = function(node) {
+ if (!(node.id in this.nodePoints)) {
+ var mass = (node.data.mass !== undefined) ? node.data.mass : 1.0;
+ this.nodePoints[node.id] = new Layout.ForceDirected.Point(Vector.random(), mass);
+ }
+
+ return this.nodePoints[node.id];
+ };
+
+ Layout.ForceDirected.prototype.spring = function(edge) {
+ if (!(edge.id in this.edgeSprings)) {
+ var length = (edge.data.length !== undefined) ? edge.data.length : 1.0;
+
+ var existingSpring = false;
+
+ var from = this.graph.getEdges(edge.source, edge.target);
+ from.forEach(function(e) {
+ if (existingSpring === false && e.id in this.edgeSprings) {
+ existingSpring = this.edgeSprings[e.id];
+ }
+ }, this);
+
+ if (existingSpring !== false) {
+ return new Layout.ForceDirected.Spring(existingSpring.point1, existingSpring.point2, 0.0, 0.0);
+ }
+
+ var to = this.graph.getEdges(edge.target, edge.source);
+ from.forEach(function(e){
+ if (existingSpring === false && e.id in this.edgeSprings) {
+ existingSpring = this.edgeSprings[e.id];
+ }
+ }, this);
+
+ if (existingSpring !== false) {
+ return new Layout.ForceDirected.Spring(existingSpring.point2, existingSpring.point1, 0.0, 0.0);
+ }
+
+ this.edgeSprings[edge.id] = new Layout.ForceDirected.Spring(
+ this.point(edge.source), this.point(edge.target), length, this.stiffness
+ );
+ }
+
+ return this.edgeSprings[edge.id];
+ };
+
+ // callback should accept two arguments: Node, Point
+ Layout.ForceDirected.prototype.eachNode = function(callback) {
+ var t = this;
+ this.graph.nodes.forEach(function(n){
+ callback.call(t, n, t.point(n));
+ });
+ };
+
+ // callback should accept two arguments: Edge, Spring
+ Layout.ForceDirected.prototype.eachEdge = function(callback) {
+ var t = this;
+ this.graph.edges.forEach(function(e){
+ callback.call(t, e, t.spring(e));
+ });
+ };
+
+ // callback should accept one argument: Spring
+ Layout.ForceDirected.prototype.eachSpring = function(callback) {
+ var t = this;
+ this.graph.edges.forEach(function(e){
+ callback.call(t, t.spring(e));
+ });
+ };
+
+
+ // Physics stuff
+ Layout.ForceDirected.prototype.applyCoulombsLaw = function() {
+ this.eachNode(function(n1, point1) {
+ this.eachNode(function(n2, point2) {
+ if (point1 !== point2)
+ {
+ var d = point1.p.subtract(point2.p);
+ var distance = d.magnitude() + 0.1; // avoid massive forces at small distances (and divide by zero)
+ var direction = d.normalise();
+
+ // apply force to each end point
+ point1.applyForce(direction.multiply(this.repulsion).divide(distance * distance * 0.5));
+ point2.applyForce(direction.multiply(this.repulsion).divide(distance * distance * -0.5));
+ }
+ });
+ });
+ };
+
+ Layout.ForceDirected.prototype.applyHookesLaw = function() {
+ this.eachSpring(function(spring){
+ var d = spring.point2.p.subtract(spring.point1.p); // the direction of the spring
+ var displacement = spring.length - d.magnitude();
+ var direction = d.normalise();
+
+ // apply force to each end point
+ spring.point1.applyForce(direction.multiply(spring.k * displacement * -0.5));
+ spring.point2.applyForce(direction.multiply(spring.k * displacement * 0.5));
+ });
+ };
+
+ Layout.ForceDirected.prototype.attractToCentre = function() {
+ this.eachNode(function(node, point) {
+ var direction = point.p.multiply(-1.0);
+ point.applyForce(direction.multiply(this.repulsion / 50.0));
+ });
+ };
+
+
+ Layout.ForceDirected.prototype.updateVelocity = function(timestep) {
+ this.eachNode(function(node, point) {
+ // Is this, along with updatePosition below, the only places that your
+ // integration code exist?
+ point.v = point.v.add(point.a.multiply(timestep)).multiply(this.damping);
+ if (point.v.magnitude() > this.maxSpeed) {
+ point.v = point.v.normalise().multiply(this.maxSpeed);
+ }
+ point.a = new Vector(0,0);
+ });
+ };
+
+ Layout.ForceDirected.prototype.updatePosition = function(timestep) {
+ this.eachNode(function(node, point) {
+ // Same question as above; along with updateVelocity, is this all of
+ // your integration code?
+ point.p = point.p.add(point.v.multiply(timestep));
+ });
+ };
+
+ // Calculate the total kinetic energy of the system
+ Layout.ForceDirected.prototype.totalEnergy = function(timestep) {
+ var energy = 0.0;
+ this.eachNode(function(node, point) {
+ var speed = point.v.magnitude();
+ energy += 0.5 * point.m * speed * speed;
+ });
+
+ return energy;
+ };
+
+ var __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; // stolen from coffeescript, thanks jashkenas! ;-)
+
+ Springy.requestAnimationFrame = __bind(this.requestAnimationFrame ||
+ this.webkitRequestAnimationFrame ||
+ this.mozRequestAnimationFrame ||
+ this.oRequestAnimationFrame ||
+ this.msRequestAnimationFrame ||
+ (function(callback, element) {
+ this.setTimeout(callback, 10);
+ }), this);
+
+
+ /**
+ * Start simulation if it's not running already.
+ * In case it's running then the call is ignored, and none of the callbacks passed is ever executed.
+ */
+ Layout.ForceDirected.prototype.start = function(render, onRenderStop, onRenderStart) {
+ var t = this;
+
+ if (this._started) return;
+ this._started = true;
+ this._stop = false;
+
+ if (onRenderStart !== undefined) { onRenderStart(); }
+
+ Springy.requestAnimationFrame(function step() {
+ t.tick(0.03);
+
+ if (render !== undefined) {
+ render();
+ }
+
+ // stop simulation when energy of the system goes below a threshold
+ if (t._stop || t.totalEnergy() < t.minEnergyThreshold) {
+ t._started = false;
+ if (onRenderStop !== undefined) { onRenderStop(); }
+ } else {
+ Springy.requestAnimationFrame(step);
+ }
+ });
+ };
+
+ Layout.ForceDirected.prototype.stop = function() {
+ this._stop = true;
+ }
+
+ Layout.ForceDirected.prototype.tick = function(timestep) {
+ this.applyCoulombsLaw();
+ this.applyHookesLaw();
+ this.attractToCentre();
+ this.updateVelocity(timestep);
+ this.updatePosition(timestep);
+ };
+
+ // Find the nearest point to a particular position
+ Layout.ForceDirected.prototype.nearest = function(pos) {
+ var min = {node: null, point: null, distance: null};
+ var t = this;
+ this.graph.nodes.forEach(function(n){
+ var point = t.point(n);
+ var distance = point.p.subtract(pos).magnitude();
+
+ if (min.distance === null || distance < min.distance) {
+ min = {node: n, point: point, distance: distance};
+ }
+ });
+
+ return min;
+ };
+
+ // returns [bottomleft, topright]
+ Layout.ForceDirected.prototype.getBoundingBox = function() {
+ var bottomleft = new Vector(-2,-2);
+ var topright = new Vector(2,2);
+
+ this.eachNode(function(n, point) {
+ if (point.p.x < bottomleft.x) {
+ bottomleft.x = point.p.x;
+ }
+ if (point.p.y < bottomleft.y) {
+ bottomleft.y = point.p.y;
+ }
+ if (point.p.x > topright.x) {
+ topright.x = point.p.x;
+ }
+ if (point.p.y > topright.y) {
+ topright.y = point.p.y;
+ }
+ });
+
+ var padding = topright.subtract(bottomleft).multiply(0.07); // ~5% padding
+
+ return {bottomleft: bottomleft.subtract(padding), topright: topright.add(padding)};
+ };
+
+
+ // Vector
+ var Vector = Springy.Vector = function(x, y) {
+ this.x = x;
+ this.y = y;
+ };
+
+ Vector.random = function() {
+ return new Vector(10.0 * (Math.random() - 0.5), 10.0 * (Math.random() - 0.5));
+ };
+
+ Vector.prototype.add = function(v2) {
+ return new Vector(this.x + v2.x, this.y + v2.y);
+ };
+
+ Vector.prototype.subtract = function(v2) {
+ return new Vector(this.x - v2.x, this.y - v2.y);
+ };
+
+ Vector.prototype.multiply = function(n) {
+ return new Vector(this.x * n, this.y * n);
+ };
+
+ Vector.prototype.divide = function(n) {
+ return new Vector((this.x / n) || 0, (this.y / n) || 0); // Avoid divide by zero errors..
+ };
+
+ Vector.prototype.magnitude = function() {
+ return Math.sqrt(this.x*this.x + this.y*this.y);
+ };
+
+ Vector.prototype.normal = function() {
+ return new Vector(-this.y, this.x);
+ };
+
+ Vector.prototype.normalise = function() {
+ return this.divide(this.magnitude());
+ };
+
+ // Point
+ Layout.ForceDirected.Point = function(position, mass) {
+ this.p = position; // position
+ this.m = mass; // mass
+ this.v = new Vector(0, 0); // velocity
+ this.a = new Vector(0, 0); // acceleration
+ };
+
+ Layout.ForceDirected.Point.prototype.applyForce = function(force) {
+ this.a = this.a.add(force.divide(this.m));
+ };
+
+ // Spring
+ Layout.ForceDirected.Spring = function(point1, point2, length, k) {
+ this.point1 = point1;
+ this.point2 = point2;
+ this.length = length; // spring length at rest
+ this.k = k; // spring constant (See Hooke's law) .. how stiff the spring is
+ };
+
+ // Layout.ForceDirected.Spring.prototype.distanceToPoint = function(point)
+ // {
+ // // hardcore vector arithmetic.. ohh yeah!
+ // // .. see http://stackoverflow.com/questions/849211/shortest-distance-between-a-point-and-a-line-segment/865080#865080
+ // var n = this.point2.p.subtract(this.point1.p).normalise().normal();
+ // var ac = point.p.subtract(this.point1.p);
+ // return Math.abs(ac.x * n.x + ac.y * n.y);
+ // };
+
+ /**
+ * Renderer handles the layout rendering loop
+ * @param onRenderStop optional callback function that gets executed whenever rendering stops.
+ * @param onRenderStart optional callback function that gets executed whenever rendering starts.
+ * @param onRenderFrame optional callback function that gets executed after each frame is rendered.
+ */
+ var Renderer = Springy.Renderer = function(layout, clear, drawEdge, drawNode, onRenderStop, onRenderStart, onRenderFrame) {
+ this.layout = layout;
+ this.clear = clear;
+ this.drawEdge = drawEdge;
+ this.drawNode = drawNode;
+ this.onRenderStop = onRenderStop;
+ this.onRenderStart = onRenderStart;
+ this.onRenderFrame = onRenderFrame;
+
+ this.layout.graph.addGraphListener(this);
+ }
+
+ Renderer.prototype.graphChanged = function(e) {
+ this.start();
+ };
+
+ /**
+ * Starts the simulation of the layout in use.
+ *
+ * Note that in case the algorithm is still or already running then the layout that's in use
+ * might silently ignore the call, and your optional
done
callback is never executed.
+ * At least the built-in ForceDirected layout behaves in this way.
+ *
+ * @param done An optional callback function that gets executed when the springy algorithm stops,
+ * either because it ended or because stop() was called.
+ */
+ Renderer.prototype.start = function(done) {
+ var t = this;
+ this.layout.start(function render() {
+ t.clear();
+
+ t.layout.eachEdge(function(edge, spring) {
+ t.drawEdge(edge, spring.point1.p, spring.point2.p);
+ });
+
+ t.layout.eachNode(function(node, point) {
+ t.drawNode(node, point.p);
+ });
+
+ if (t.onRenderFrame !== undefined) { t.onRenderFrame(); }
+ }, this.onRenderStop, this.onRenderStart);
+ };
+
+ Renderer.prototype.stop = function() {
+ this.layout.stop();
+ };
+
+ // Array.forEach implementation for IE support..
+ //https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/forEach
+ if ( !Array.prototype.forEach ) {
+ Array.prototype.forEach = function( callback, thisArg ) {
+ var T, k;
+ if ( this == null ) {
+ throw new TypeError( " this is null or not defined" );
+ }
+ var O = Object(this);
+ var len = O.length >>> 0; // Hack to convert O.length to a UInt32
+ if ( {}.toString.call(callback) != "[object Function]" ) {
+ throw new TypeError( callback + " is not a function" );
+ }
+ if ( thisArg ) {
+ T = thisArg;
+ }
+ k = 0;
+ while( k < len ) {
+ var kValue;
+ if ( k in O ) {
+ kValue = O[ k ];
+ callback.call( T, kValue, k, O );
+ }
+ k++;
+ }
+ };
+ }
+
+ var isEmpty = function(obj) {
+ for (var k in obj) {
+ if (obj.hasOwnProperty(k)) {
+ return false;
+ }
+ }
+ return true;
+ };
+
+ return Springy;
+}));
diff --git a/springyui.js b/springyui.js
new file mode 100755
index 0000000..acc35eb
--- /dev/null
+++ b/springyui.js
@@ -0,0 +1,394 @@
+/**
+Copyright (c) 2010 Dennis Hotson
+
+ Permission is hereby granted, free of charge, to any person
+ obtaining a copy of this software and associated documentation
+ files (the "Software"), to deal in the Software without
+ restriction, including without limitation the rights to use,
+ copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the
+ Software is furnished to do so, subject to the following
+ conditions:
+
+ The above copyright notice and this permission notice shall be
+ included in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ OTHER DEALINGS IN THE SOFTWARE.
+*/
+
+(function() {
+
+jQuery.fn.springy = function(params) {
+ var graph = this.graph = params.graph || new Springy.Graph();
+ var nodeFont = "16px Verdana, sans-serif";
+ var edgeFont = "8px Verdana, sans-serif";
+ var stiffness = params.stiffness || 400.0;
+ var repulsion = params.repulsion || 400.0;
+ var damping = params.damping || 0.5;
+ var minEnergyThreshold = params.minEnergyThreshold || 0.00001;
+ var nodeSelected = params.nodeSelected || null;
+ var nodeImages = {};
+ var edgeLabelsUpright = true;
+
+ var canvas = this[0];
+ var ctx = canvas.getContext("2d");
+
+ var layout = this.layout = new Springy.Layout.ForceDirected(graph, stiffness, repulsion, damping, minEnergyThreshold);
+
+ // calculate bounding box of graph layout.. with ease-in
+ var currentBB = layout.getBoundingBox();
+ var targetBB = {bottomleft: new Springy.Vector(-2, -2), topright: new Springy.Vector(2, 2)};
+
+ // auto adjusting bounding box
+ Springy.requestAnimationFrame(function adjust() {
+ targetBB = layout.getBoundingBox();
+ // current gets 20% closer to target every iteration
+ currentBB = {
+ bottomleft: currentBB.bottomleft.add( targetBB.bottomleft.subtract(currentBB.bottomleft)
+ .divide(10)),
+ topright: currentBB.topright.add( targetBB.topright.subtract(currentBB.topright)
+ .divide(10))
+ };
+
+ Springy.requestAnimationFrame(adjust);
+ });
+
+ // convert to/from screen coordinates
+ var toScreen = function(p) {
+ var size = currentBB.topright.subtract(currentBB.bottomleft);
+ var sx = p.subtract(currentBB.bottomleft).divide(size.x).x * canvas.width;
+ var sy = p.subtract(currentBB.bottomleft).divide(size.y).y * canvas.height;
+ return new Springy.Vector(sx, sy);
+ };
+
+ var fromScreen = function(s) {
+ var size = currentBB.topright.subtract(currentBB.bottomleft);
+ var px = (s.x / canvas.width) * size.x + currentBB.bottomleft.x;
+ var py = (s.y / canvas.height) * size.y + currentBB.bottomleft.y;
+ return new Springy.Vector(px, py);
+ };
+
+ // half-assed drag and drop
+ var selected = null;
+ var nearest = null;
+ var dragged = null;
+
+ jQuery(canvas).mousedown(function(e) {
+ var pos = jQuery(this).offset();
+ var p = fromScreen({x: e.pageX - pos.left, y: e.pageY - pos.top});
+ selected = nearest = dragged = layout.nearest(p);
+
+ if (selected.node !== null) {
+ dragged.point.m = 10000.0;
+
+ if (nodeSelected) {
+ nodeSelected(selected.node);
+ }
+ }
+
+ renderer.start();
+ });
+
+ // Basic double click handler
+ jQuery(canvas).dblclick(function(e) {
+ var pos = jQuery(this).offset();
+ var p = fromScreen({x: e.pageX - pos.left, y: e.pageY - pos.top});
+ selected = layout.nearest(p);
+ node = selected.node;
+ if (node && node.data && node.data.ondoubleclick) {
+ node.data.ondoubleclick();
+ }
+ });
+
+ jQuery(canvas).mousemove(function(e) {
+ var pos = jQuery(this).offset();
+ var p = fromScreen({x: e.pageX - pos.left, y: e.pageY - pos.top});
+ nearest = layout.nearest(p);
+
+ if (dragged !== null && dragged.node !== null) {
+ dragged.point.p.x = p.x;
+ dragged.point.p.y = p.y;
+ }
+
+ renderer.start();
+ });
+
+ jQuery(window).bind('mouseup',function(e) {
+ dragged = null;
+ });
+
+ var getTextWidth = function(node) {
+ var text = (node.data.label !== undefined) ? node.data.label : node.id;
+ if (node._width && node._width[text])
+ return node._width[text];
+
+ ctx.save();
+ ctx.font = (node.data.font !== undefined) ? node.data.font : nodeFont;
+ var width = ctx.measureText(text).width;
+ ctx.restore();
+
+ node._width || (node._width = {});
+ node._width[text] = width;
+
+ return width;
+ };
+
+ var getTextHeight = function(node) {
+ return 16;
+ // In a more modular world, this would actually read the font size, but I think leaving it a constant is sufficient for now.
+ // If you change the font size, I'd adjust this too.
+ };
+
+ var getImageWidth = function(node) {
+ var width = (node.data.image.width !== undefined) ? node.data.image.width : nodeImages[node.data.image.src].object.width;
+ return width;
+ }
+
+ var getImageHeight = function(node) {
+ var height = (node.data.image.height !== undefined) ? node.data.image.height : nodeImages[node.data.image.src].object.height;
+ return height;
+ }
+
+ Springy.Node.prototype.getHeight = function() {
+ var height;
+ if (this.data.image == undefined) {
+ height = getTextHeight(this);
+ } else {
+ if (this.data.image.src in nodeImages && nodeImages[this.data.image.src].loaded) {
+ height = getImageHeight(this);
+ } else {height = 10;}
+ }
+ return height;
+ }
+
+ Springy.Node.prototype.getWidth = function() {
+ var width;
+ if (this.data.image == undefined) {
+ width = getTextWidth(this);
+ } else {
+ if (this.data.image.src in nodeImages && nodeImages[this.data.image.src].loaded) {
+ width = getImageWidth(this);
+ } else {width = 10;}
+ }
+ return width;
+ }
+
+ var renderer = this.renderer = new Springy.Renderer(layout,
+ function clear() {
+ ctx.clearRect(0,0,canvas.width,canvas.height);
+ },
+ function drawEdge(edge, p1, p2) {
+ var x1 = toScreen(p1).x;
+ var y1 = toScreen(p1).y;
+ var x2 = toScreen(p2).x;
+ var y2 = toScreen(p2).y;
+
+ var direction = new Springy.Vector(x2-x1, y2-y1);
+ var normal = direction.normal().normalise();
+
+ var from = graph.getEdges(edge.source, edge.target);
+ var to = graph.getEdges(edge.target, edge.source);
+
+ var total = from.length + to.length;
+
+ // Figure out edge's position in relation to other edges between the same nodes
+ var n = 0;
+ for (var i=0; i
Math.PI/2 || angle < -Math.PI/2)) {
+ displacement = 8;
+ angle += Math.PI;
+ }
+ var textPos = s1.add(s2).divide(2).add(normal.multiply(displacement));
+ ctx.translate(textPos.x, textPos.y);
+ ctx.rotate(angle);
+ ctx.fillText(text, 0,-2);
+ ctx.restore();
+ }
+
+ },
+ function drawNode(node, p) {
+ var s = toScreen(p);
+
+ ctx.save();
+
+ // Pulled out the padding aspect sso that the size functions could be used in multiple places
+ // These should probably be settable by the user (and scoped higher) but this suffices for now
+ var paddingX = 6;
+ var paddingY = 6;
+
+ var contentWidth = node.getWidth();
+ var contentHeight = node.getHeight();
+ var boxWidth = contentWidth + paddingX;
+ var boxHeight = contentHeight + paddingY;
+
+ // clear background
+ ctx.clearRect(s.x - boxWidth/2, s.y - boxHeight/2, boxWidth, boxHeight);
+
+ // fill background
+ if (selected !== null && selected.node !== null && selected.node.id === node.id) {
+ ctx.fillStyle = "#FFFFE0";
+ } else if (nearest !== null && nearest.node !== null && nearest.node.id === node.id) {
+ ctx.fillStyle = "#EEEEEE";
+ } else {
+ ctx.fillStyle = "#FFFFFF";
+ }
+ ctx.fillRect(s.x - boxWidth/2, s.y - boxHeight/2, boxWidth, boxHeight);
+
+ if (node.data.image == undefined) {
+ ctx.textAlign = "left";
+ ctx.textBaseline = "top";
+ ctx.font = (node.data.font !== undefined) ? node.data.font : nodeFont;
+ ctx.fillStyle = (node.data.color !== undefined) ? node.data.color : "#000000";
+ var text = (node.data.label !== undefined) ? node.data.label : node.id;
+ ctx.fillText(text, s.x - contentWidth/2, s.y - contentHeight/2);
+ } else {
+ // Currently we just ignore any labels if the image object is set. One might want to extend this logic to allow for both, or other composite nodes.
+ var src = node.data.image.src; // There should probably be a sanity check here too, but un-src-ed images aren't exaclty a disaster.
+ if (src in nodeImages) {
+ if (nodeImages[src].loaded) {
+ // Our image is loaded, so it's safe to draw
+ ctx.drawImage(nodeImages[src].object, s.x - contentWidth/2, s.y - contentHeight/2, contentWidth, contentHeight);
+ }
+ }else{
+ // First time seeing an image with this src address, so add it to our set of image objects
+ // Note: we index images by their src to avoid making too many duplicates
+ nodeImages[src] = {};
+ var img = new Image();
+ nodeImages[src].object = img;
+ img.addEventListener("load", function () {
+ // HTMLImageElement objects are very finicky about being used before they are loaded, so we set a flag when it is done
+ nodeImages[src].loaded = true;
+ });
+ img.src = src;
+ }
+ }
+ ctx.restore();
+ }
+ );
+
+ renderer.start();
+
+ // helpers for figuring out where to draw arrows
+ function intersect_line_line(p1, p2, p3, p4) {
+ var denom = ((p4.y - p3.y)*(p2.x - p1.x) - (p4.x - p3.x)*(p2.y - p1.y));
+
+ // lines are parallel
+ if (denom === 0) {
+ return false;
+ }
+
+ var ua = ((p4.x - p3.x)*(p1.y - p3.y) - (p4.y - p3.y)*(p1.x - p3.x)) / denom;
+ var ub = ((p2.x - p1.x)*(p1.y - p3.y) - (p2.y - p1.y)*(p1.x - p3.x)) / denom;
+
+ if (ua < 0 || ua > 1 || ub < 0 || ub > 1) {
+ return false;
+ }
+
+ return new Springy.Vector(p1.x + ua * (p2.x - p1.x), p1.y + ua * (p2.y - p1.y));
+ }
+
+ function intersect_line_box(p1, p2, p3, w, h) {
+ var tl = {x: p3.x, y: p3.y};
+ var tr = {x: p3.x + w, y: p3.y};
+ var bl = {x: p3.x, y: p3.y + h};
+ var br = {x: p3.x + w, y: p3.y + h};
+
+ var result;
+ if (result = intersect_line_line(p1, p2, tl, tr)) { return result; } // top
+ if (result = intersect_line_line(p1, p2, tr, br)) { return result; } // right
+ if (result = intersect_line_line(p1, p2, br, bl)) { return result; } // bottom
+ if (result = intersect_line_line(p1, p2, bl, tl)) { return result; } // left
+
+ return false;
+ }
+
+ return this;
+}
+
+})();
diff --git a/test/channel-messaging-basic/index.html b/test/channel-messaging-basic/index.html
new file mode 100644
index 0000000..70d6e29
--- /dev/null
+++ b/test/channel-messaging-basic/index.html
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+ Channel messaging demo
+
+
+
+ Channel messaging demo
+ Index.html para (I will be overwritten)
+
+
+
+
diff --git a/test/channel-messaging-basic/page2.html b/test/channel-messaging-basic/page2.html
new file mode 100644
index 0000000..2cc36da
--- /dev/null
+++ b/test/channel-messaging-basic/page2.html
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+ Page 2
+
+
+
+ page2.html (iframe body)
+
+
+
diff --git a/test/fetch-text/index.html b/test/fetch-text/index.html
new file mode 100644
index 0000000..82868c0
--- /dev/null
+++ b/test/fetch-text/index.html
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+
+ Fetch text example
+
+
+
+
+
+ Fetch text example
+
+
+
+
+
+
diff --git a/test/fetch-text/page1.txt b/test/fetch-text/page1.txt
new file mode 100644
index 0000000..c2dd9ae
--- /dev/null
+++ b/test/fetch-text/page1.txt
@@ -0,0 +1,3 @@
+Page 1: A desparate journey
+
+This is the first exciting part of our story! Once upon a time, a man was creeping into a dark forest, to try to find the woodcutter's cottage. Somewhere, an owl hooted. A feeling of lingering dread spread all over his body, but he pressed on regardless.
diff --git a/test/fetch-text/page2.txt b/test/fetch-text/page2.txt
new file mode 100644
index 0000000..a4a8e61
--- /dev/null
+++ b/test/fetch-text/page2.txt
@@ -0,0 +1,3 @@
+Page 2: Light at the end of the tunnel
+
+Brambles tugged at his clothes, and the rain lashed down, making our hero bruised and cold. when hope was all but lost, he noticed a dim light shining through the trees — this must be the woodcutter's cottage. This very sight spurred him to continue.
diff --git a/test/fetch-text/page3.txt b/test/fetch-text/page3.txt
new file mode 100644
index 0000000..a36dfef
--- /dev/null
+++ b/test/fetch-text/page3.txt
@@ -0,0 +1,3 @@
+Page 3: The end unseen?
+
+A kilometer or so more, and the cottage was now in sight — our hero could see lights shining in the windows, and a figure shuffling around within! This was the moment he was waiting for surely! He tiptoed up to the door, and carefully tried the handle. Fnding it unlocked, he burst open the door and shouted "your time has come, scoundrel..!" He was slightly disappointed to come face to face with a slightly scared dog.
diff --git a/test/fetch-text/style.css b/test/fetch-text/style.css
new file mode 100644
index 0000000..f8a1eaf
--- /dev/null
+++ b/test/fetch-text/style.css
@@ -0,0 +1,51 @@
+html {
+ font-family: sans-serif;
+}
+
+h1,
+h2 {
+ text-align: center;
+}
+
+article {
+ width: 400px;
+ min-height: 480px;
+ margin: 0 auto;
+ padding: 10px;
+ background-image: repeating-linear-gradient(
+ to bottom,
+ transparent 1px,
+ transparent 20px,
+ rgb(0, 0, 150) 21px
+ ),
+ linear-gradient(to bottom right, white, #ccc);
+ border-radius: 20px;
+ box-shadow: 5px 5px 10px rgba(0, 0, 0, 0.7);
+ line-height: 1.5;
+}
+
+ul {
+ list-style-type: none;
+ padding-left: 0;
+ width: 480px;
+ margin: 0 auto;
+ padding-bottom: 30px;
+}
+
+li {
+ float: left;
+ width: 33%;
+}
+
+ul li a {
+ display: block;
+ text-align: center;
+ color: blue;
+ text-decoration: underline;
+ cursor: pointer;
+}
+
+ul li a:hover,
+ul li a:focus {
+ text-decoration: none;
+}
diff --git a/test/springy/2d.html b/test/springy/2d.html
new file mode 100644
index 0000000..606cee5
--- /dev/null
+++ b/test/springy/2d.html
@@ -0,0 +1,106 @@
+
+
+
+
+
+handle
+
+
+
+
+
+
+
+
+
+start game
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/springy/jquery-3.7.1.min.js b/test/springy/jquery-3.7.1.min.js
new file mode 100644
index 0000000..7f37b5d
--- /dev/null
+++ b/test/springy/jquery-3.7.1.min.js
@@ -0,0 +1,2 @@
+/*! jQuery v3.7.1 | (c) OpenJS Foundation and other contributors | jquery.org/license */
+!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(ie,e){"use strict";var oe=[],r=Object.getPrototypeOf,ae=oe.slice,g=oe.flat?function(e){return oe.flat.call(e)}:function(e){return oe.concat.apply([],e)},s=oe.push,se=oe.indexOf,n={},i=n.toString,ue=n.hasOwnProperty,o=ue.toString,a=o.call(Object),le={},v=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType&&"function"!=typeof e.item},y=function(e){return null!=e&&e===e.window},C=ie.document,u={type:!0,src:!0,nonce:!0,noModule:!0};function m(e,t,n){var r,i,o=(n=n||C).createElement("script");if(o.text=e,t)for(r in u)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function x(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[i.call(e)]||"object":typeof e}var t="3.7.1",l=/HTML$/i,ce=function(e,t){return new ce.fn.init(e,t)};function c(e){var t=!!e&&"length"in e&&e.length,n=x(e);return!v(e)&&!y(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+ge+")"+ge+"*"),x=new RegExp(ge+"|>"),j=new RegExp(g),A=new RegExp("^"+t+"$"),D={ID:new RegExp("^#("+t+")"),CLASS:new RegExp("^\\.("+t+")"),TAG:new RegExp("^("+t+"|[*])"),ATTR:new RegExp("^"+p),PSEUDO:new RegExp("^"+g),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+ge+"*(even|odd|(([+-]|)(\\d*)n|)"+ge+"*(?:([+-]|)"+ge+"*(\\d+)|))"+ge+"*\\)|)","i"),bool:new RegExp("^(?:"+f+")$","i"),needsContext:new RegExp("^"+ge+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+ge+"*((?:-\\d)?\\d*)"+ge+"*\\)|)(?=[^-]|$)","i")},N=/^(?:input|select|textarea|button)$/i,q=/^h\d$/i,L=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,H=/[+~]/,O=new RegExp("\\\\[\\da-fA-F]{1,6}"+ge+"?|\\\\([^\\r\\n\\f])","g"),P=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},M=function(){V()},R=J(function(e){return!0===e.disabled&&fe(e,"fieldset")},{dir:"parentNode",next:"legend"});try{k.apply(oe=ae.call(ye.childNodes),ye.childNodes),oe[ye.childNodes.length].nodeType}catch(e){k={apply:function(e,t){me.apply(e,ae.call(t))},call:function(e){me.apply(e,ae.call(arguments,1))}}}function I(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(V(e),e=e||T,C)){if(11!==p&&(u=L.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return k.call(n,a),n}else if(f&&(a=f.getElementById(i))&&I.contains(e,a)&&a.id===i)return k.call(n,a),n}else{if(u[2])return k.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&e.getElementsByClassName)return k.apply(n,e.getElementsByClassName(i)),n}if(!(h[t+" "]||d&&d.test(t))){if(c=t,f=e,1===p&&(x.test(t)||m.test(t))){(f=H.test(t)&&U(e.parentNode)||e)==e&&le.scope||((s=e.getAttribute("id"))?s=ce.escapeSelector(s):e.setAttribute("id",s=S)),o=(l=Y(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+Q(l[o]);c=l.join(",")}try{return k.apply(n,f.querySelectorAll(c)),n}catch(e){h(t,!0)}finally{s===S&&e.removeAttribute("id")}}}return re(t.replace(ve,"$1"),e,n,r)}function W(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function F(e){return e[S]=!0,e}function $(e){var t=T.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function B(t){return function(e){return fe(e,"input")&&e.type===t}}function _(t){return function(e){return(fe(e,"input")||fe(e,"button"))&&e.type===t}}function z(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&R(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function X(a){return F(function(o){return o=+o,F(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function U(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}function V(e){var t,n=e?e.ownerDocument||e:ye;return n!=T&&9===n.nodeType&&n.documentElement&&(r=(T=n).documentElement,C=!ce.isXMLDoc(T),i=r.matches||r.webkitMatchesSelector||r.msMatchesSelector,r.msMatchesSelector&&ye!=T&&(t=T.defaultView)&&t.top!==t&&t.addEventListener("unload",M),le.getById=$(function(e){return r.appendChild(e).id=ce.expando,!T.getElementsByName||!T.getElementsByName(ce.expando).length}),le.disconnectedMatch=$(function(e){return i.call(e,"*")}),le.scope=$(function(){return T.querySelectorAll(":scope")}),le.cssHas=$(function(){try{return T.querySelector(":has(*,:jqfake)"),!1}catch(e){return!0}}),le.getById?(b.filter.ID=function(e){var t=e.replace(O,P);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&C){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(O,P);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&C){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):t.querySelectorAll(e)},b.find.CLASS=function(e,t){if("undefined"!=typeof t.getElementsByClassName&&C)return t.getElementsByClassName(e)},d=[],$(function(e){var t;r.appendChild(e).innerHTML="",e.querySelectorAll("[selected]").length||d.push("\\["+ge+"*(?:value|"+f+")"),e.querySelectorAll("[id~="+S+"-]").length||d.push("~="),e.querySelectorAll("a#"+S+"+*").length||d.push(".#.+[+~]"),e.querySelectorAll(":checked").length||d.push(":checked"),(t=T.createElement("input")).setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),r.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&d.push(":enabled",":disabled"),(t=T.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||d.push("\\["+ge+"*name"+ge+"*="+ge+"*(?:''|\"\")")}),le.cssHas||d.push(":has"),d=d.length&&new RegExp(d.join("|")),l=function(e,t){if(e===t)return a=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!le.sortDetached&&t.compareDocumentPosition(e)===n?e===T||e.ownerDocument==ye&&I.contains(ye,e)?-1:t===T||t.ownerDocument==ye&&I.contains(ye,t)?1:o?se.call(o,e)-se.call(o,t):0:4&n?-1:1)}),T}for(e in I.matches=function(e,t){return I(e,null,null,t)},I.matchesSelector=function(e,t){if(V(e),C&&!h[t+" "]&&(!d||!d.test(t)))try{var n=i.call(e,t);if(n||le.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){h(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(O,P),e[3]=(e[3]||e[4]||e[5]||"").replace(O,P),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||I.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&I.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return D.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&j.test(n)&&(t=Y(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(O,P).toLowerCase();return"*"===e?function(){return!0}:function(e){return fe(e,t)}},CLASS:function(e){var t=s[e+" "];return t||(t=new RegExp("(^|"+ge+")"+e+"("+ge+"|$)"))&&s(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=I.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function T(e,n,r){return v(n)?ce.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?ce.grep(e,function(e){return e===n!==r}):"string"!=typeof n?ce.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(ce.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||k,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:S.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof ce?t[0]:t,ce.merge(this,ce.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:C,!0)),w.test(r[1])&&ce.isPlainObject(t))for(r in t)v(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=C.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):v(e)?void 0!==n.ready?n.ready(e):e(ce):ce.makeArray(e,this)}).prototype=ce.fn,k=ce(C);var E=/^(?:parents|prev(?:Until|All))/,j={children:!0,contents:!0,next:!0,prev:!0};function A(e,t){while((e=e[t])&&1!==e.nodeType);return e}ce.fn.extend({has:function(e){var t=ce(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,Ce=/^$|^module$|\/(?:java|ecma)script/i;xe=C.createDocumentFragment().appendChild(C.createElement("div")),(be=C.createElement("input")).setAttribute("type","radio"),be.setAttribute("checked","checked"),be.setAttribute("name","t"),xe.appendChild(be),le.checkClone=xe.cloneNode(!0).cloneNode(!0).lastChild.checked,xe.innerHTML="",le.noCloneChecked=!!xe.cloneNode(!0).lastChild.defaultValue,xe.innerHTML="",le.option=!!xe.lastChild;var ke={thead:[1,""],col:[2,""],tr:[2,""],td:[3,""],_default:[0,"",""]};function Se(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&fe(e,t)?ce.merge([e],n):n}function Ee(e,t){for(var n=0,r=e.length;n",""]);var je=/<|?\w+;/;function Ae(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d\s*$/g;function Re(e,t){return fe(e,"table")&&fe(11!==t.nodeType?t:t.firstChild,"tr")&&ce(e).children("tbody")[0]||e}function Ie(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function We(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Fe(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(_.hasData(e)&&(s=_.get(e).events))for(i in _.remove(t,"handle events"),s)for(n=0,r=s[i].length;n").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),C.head.appendChild(r[0])},abort:function(){i&&i()}}});var Jt,Kt=[],Zt=/(=)\?(?=&|$)|\?\?/;ce.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=Kt.pop()||ce.expando+"_"+jt.guid++;return this[e]=!0,e}}),ce.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Zt.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Zt.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=v(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Zt,"$1"+r):!1!==e.jsonp&&(e.url+=(At.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||ce.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=ie[r],ie[r]=function(){o=arguments},n.always(function(){void 0===i?ce(ie).removeProp(r):ie[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,Kt.push(r)),o&&v(i)&&i(o[0]),o=i=void 0}),"script"}),le.createHTMLDocument=((Jt=C.implementation.createHTMLDocument("").body).innerHTML="",2===Jt.childNodes.length),ce.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(le.createHTMLDocument?((r=(t=C.implementation.createHTMLDocument("")).createElement("base")).href=C.location.href,t.head.appendChild(r)):t=C),o=!n&&[],(i=w.exec(e))?[t.createElement(i[1])]:(i=Ae([e],t,o),o&&o.length&&ce(o).remove(),ce.merge([],i.childNodes)));var r,i,o},ce.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(ce.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},ce.expr.pseudos.animated=function(t){return ce.grep(ce.timers,function(e){return t===e.elem}).length},ce.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=ce.css(e,"position"),c=ce(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=ce.css(e,"top"),u=ce.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),v(t)&&(t=t.call(e,n,ce.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},ce.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){ce.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===ce.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===ce.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=ce(e).offset()).top+=ce.css(e,"borderTopWidth",!0),i.left+=ce.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-ce.css(r,"marginTop",!0),left:t.left-i.left-ce.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===ce.css(e,"position"))e=e.offsetParent;return e||J})}}),ce.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;ce.fn[t]=function(e){return M(this,function(e,t,n){var r;if(y(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),ce.each(["top","left"],function(e,n){ce.cssHooks[n]=Ye(le.pixelPosition,function(e,t){if(t)return t=Ge(e,n),_e.test(t)?ce(e).position()[n]+"px":t})}),ce.each({Height:"height",Width:"width"},function(a,s){ce.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){ce.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return M(this,function(e,t,n){var r;return y(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?ce.css(e,t,i):ce.style(e,t,n,i)},s,n?e:void 0,n)}})}),ce.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){ce.fn[t]=function(e){return this.on(t,e)}}),ce.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.on("mouseenter",e).on("mouseleave",t||e)}}),ce.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){ce.fn[n]=function(e,t){return 0
+*/
+header( 'Location: ./springy.html' );
+$handle = fopen("./spingy.html", "a");
+// load the data and delete the line from the array
+$lines = file('./spingy.html');
+$last = sizeof($lines) - 1 ;
+unset($lines[$last]);
+// write the new data to the file
+file_put_contents('./spingy.html', $lines);
+foreach($_POST as $variable => $value) {
+ $value = str_replace(' ', '_', $value);
+ fwrite($handle,
+ "\""
+ . $value
+ . "\""
+ . ","
+ . "]};");
+}
+fclose($handle);
+?>
\ No newline at end of file
diff --git a/test/springy/springy.html b/test/springy/springy.html
new file mode 100644
index 0000000..0450d39
--- /dev/null
+++ b/test/springy/springy.html
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/springy/springy.js b/test/springy/springy.js
new file mode 100644
index 0000000..0bf5ba4
--- /dev/null
+++ b/test/springy/springy.js
@@ -0,0 +1,735 @@
+/**
+ * Springy v2.7.1
+ *
+ * Copyright (c) 2010-2013 Dennis Hotson
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ */
+(function (root, factory) {
+ if (typeof define === 'function' && define.amd) {
+ // AMD. Register as an anonymous module.
+ define(function () {
+ return (root.returnExportsGlobal = factory());
+ });
+ } else if (typeof exports === 'object') {
+ // Node. Does not work with strict CommonJS, but
+ // only CommonJS-like enviroments that support module.exports,
+ // like Node.
+ module.exports = factory();
+ } else {
+ // Browser globals
+ root.Springy = factory();
+ }
+}(this, function() {
+
+ var Springy = {};
+
+ var Graph = Springy.Graph = function() {
+ this.nodeSet = {};
+ this.nodes = [];
+ this.edges = [];
+ this.adjacency = {};
+
+ this.nextNodeId = 0;
+ this.nextEdgeId = 0;
+ this.eventListeners = [];
+ };
+
+ var Node = Springy.Node = function(id, data) {
+ this.id = id;
+ this.data = (data !== undefined) ? data : {};
+
+ // Data fields used by layout algorithm in this file:
+ // this.data.mass
+ // Data used by default renderer in springyui.js
+ // this.data.label
+ };
+
+ var Edge = Springy.Edge = function(id, source, target, data) {
+ this.id = id;
+ this.source = source;
+ this.target = target;
+ this.data = (data !== undefined) ? data : {};
+
+ // Edge data field used by layout alorithm
+ // this.data.length
+ // this.data.type
+ };
+
+ Graph.prototype.addNode = function(node) {
+ if (!(node.id in this.nodeSet)) {
+ this.nodes.push(node);
+ }
+
+ this.nodeSet[node.id] = node;
+
+ this.notify();
+ return node;
+ };
+
+ Graph.prototype.addNodes = function() {
+ // accepts variable number of arguments, where each argument
+ // is a string that becomes both node identifier and label
+ for (var i = 0; i < arguments.length; i++) {
+ var name = arguments[i];
+ var node = new Node(name, {label:name});
+ this.addNode(node);
+ }
+ };
+
+ Graph.prototype.addEdge = function(edge) {
+ var exists = false;
+ this.edges.forEach(function(e) {
+ if (edge.id === e.id) { exists = true; }
+ });
+
+ if (!exists) {
+ this.edges.push(edge);
+ }
+
+ if (!(edge.source.id in this.adjacency)) {
+ this.adjacency[edge.source.id] = {};
+ }
+ if (!(edge.target.id in this.adjacency[edge.source.id])) {
+ this.adjacency[edge.source.id][edge.target.id] = [];
+ }
+
+ exists = false;
+ this.adjacency[edge.source.id][edge.target.id].forEach(function(e) {
+ if (edge.id === e.id) { exists = true; }
+ });
+
+ if (!exists) {
+ this.adjacency[edge.source.id][edge.target.id].push(edge);
+ }
+
+ this.notify();
+ return edge;
+ };
+
+ Graph.prototype.addEdges = function() {
+ // accepts variable number of arguments, where each argument
+ // is a triple [nodeid1, nodeid2, attributes]
+ for (var i = 0; i < arguments.length; i++) {
+ var e = arguments[i];
+ var node1 = this.nodeSet[e[0]];
+ if (node1 == undefined) {
+ throw new TypeError("invalid node name: " + e[0]);
+ }
+ var node2 = this.nodeSet[e[1]];
+ if (node2 == undefined) {
+ throw new TypeError("invalid node name: " + e[1]);
+ }
+ var attr = e[2];
+
+ this.newEdge(node1, node2, attr);
+ }
+ };
+
+ Graph.prototype.newNode = function(data) {
+ var node = new Node(this.nextNodeId++, data);
+ this.addNode(node);
+ return node;
+ };
+
+ Graph.prototype.newEdge = function(source, target, data) {
+ var edge = new Edge(this.nextEdgeId++, source, target, data);
+ this.addEdge(edge);
+ return edge;
+ };
+
+
+ // add nodes and edges from JSON object
+ Graph.prototype.loadJSON = function(json) {
+ /**
+ Springy's simple JSON format for graphs.
+
+ historically, Springy uses separate lists
+ of nodes and edges:
+
+ {
+ "nodes": [
+ "center",
+ "left",
+ "right",
+ "up",
+ "satellite"
+ ],
+ "edges": [
+ ["center", "left"],
+ ["center", "right"],
+ ["center", "up"]
+ ]
+ }
+
+ **/
+ // parse if a string is passed (EC5+ browsers)
+ if (typeof json == 'string' || json instanceof String) {
+ json = JSON.parse( json );
+ }
+
+ if ('nodes' in json || 'edges' in json) {
+ this.addNodes.apply(this, json['nodes']);
+ this.addEdges.apply(this, json['edges']);
+ }
+ }
+
+
+ // find the edges from node1 to node2
+ Graph.prototype.getEdges = function(node1, node2) {
+ if (node1.id in this.adjacency
+ && node2.id in this.adjacency[node1.id]) {
+ return this.adjacency[node1.id][node2.id];
+ }
+
+ return [];
+ };
+
+ // remove a node and it's associated edges from the graph
+ Graph.prototype.removeNode = function(node) {
+ if (node.id in this.nodeSet) {
+ delete this.nodeSet[node.id];
+ }
+
+ for (var i = this.nodes.length - 1; i >= 0; i--) {
+ if (this.nodes[i].id === node.id) {
+ this.nodes.splice(i, 1);
+ }
+ }
+
+ this.detachNode(node);
+ };
+
+ // removes edges associated with a given node
+ Graph.prototype.detachNode = function(node) {
+ var tmpEdges = this.edges.slice();
+ tmpEdges.forEach(function(e) {
+ if (e.source.id === node.id || e.target.id === node.id) {
+ this.removeEdge(e);
+ }
+ }, this);
+
+ this.notify();
+ };
+
+ // remove a node and it's associated edges from the graph
+ Graph.prototype.removeEdge = function(edge) {
+ for (var i = this.edges.length - 1; i >= 0; i--) {
+ if (this.edges[i].id === edge.id) {
+ this.edges.splice(i, 1);
+ }
+ }
+
+ for (var x in this.adjacency) {
+ for (var y in this.adjacency[x]) {
+ var edges = this.adjacency[x][y];
+
+ for (var j=edges.length - 1; j>=0; j--) {
+ if (this.adjacency[x][y][j].id === edge.id) {
+ this.adjacency[x][y].splice(j, 1);
+ }
+ }
+
+ // Clean up empty edge arrays
+ if (this.adjacency[x][y].length == 0) {
+ delete this.adjacency[x][y];
+ }
+ }
+
+ // Clean up empty objects
+ if (isEmpty(this.adjacency[x])) {
+ delete this.adjacency[x];
+ }
+ }
+
+ this.notify();
+ };
+
+ /* Merge a list of nodes and edges into the current graph. eg.
+ var o = {
+ nodes: [
+ {id: 123, data: {type: 'user', userid: 123, displayname: 'aaa'}},
+ {id: 234, data: {type: 'user', userid: 234, displayname: 'bbb'}}
+ ],
+ edges: [
+ {from: 0, to: 1, type: 'submitted_design', directed: true, data: {weight: }}
+ ]
+ }
+ */
+ Graph.prototype.merge = function(data) {
+ var nodes = [];
+ data.nodes.forEach(function(n) {
+ nodes.push(this.addNode(new Node(n.id, n.data)));
+ }, this);
+
+ data.edges.forEach(function(e) {
+ var from = nodes[e.from];
+ var to = nodes[e.to];
+
+ var id = (e.directed)
+ ? (id = e.type + "-" + from.id + "-" + to.id)
+ : (from.id < to.id) // normalise id for non-directed edges
+ ? e.type + "-" + from.id + "-" + to.id
+ : e.type + "-" + to.id + "-" + from.id;
+
+ var edge = this.addEdge(new Edge(id, from, to, e.data));
+ edge.data.type = e.type;
+ }, this);
+ };
+
+ Graph.prototype.filterNodes = function(fn) {
+ var tmpNodes = this.nodes.slice();
+ tmpNodes.forEach(function(n) {
+ if (!fn(n)) {
+ this.removeNode(n);
+ }
+ }, this);
+ };
+
+ Graph.prototype.filterEdges = function(fn) {
+ var tmpEdges = this.edges.slice();
+ tmpEdges.forEach(function(e) {
+ if (!fn(e)) {
+ this.removeEdge(e);
+ }
+ }, this);
+ };
+
+
+ Graph.prototype.addGraphListener = function(obj) {
+ this.eventListeners.push(obj);
+ };
+
+ Graph.prototype.notify = function() {
+ this.eventListeners.forEach(function(obj){
+ obj.graphChanged();
+ });
+ };
+
+ // -----------
+ var Layout = Springy.Layout = {};
+ Layout.ForceDirected = function(graph, stiffness, repulsion, damping, minEnergyThreshold, maxSpeed) {
+ this.graph = graph;
+ this.stiffness = stiffness; // spring stiffness constant
+ this.repulsion = repulsion; // repulsion constant
+ this.damping = damping; // velocity damping factor
+ this.minEnergyThreshold = minEnergyThreshold || 0.01; //threshold used to determine render stop
+ this.maxSpeed = maxSpeed || Infinity; // nodes aren't allowed to exceed this speed
+
+ this.nodePoints = {}; // keep track of points associated with nodes
+ this.edgeSprings = {}; // keep track of springs associated with edges
+ };
+
+ Layout.ForceDirected.prototype.point = function(node) {
+ if (!(node.id in this.nodePoints)) {
+ var mass = (node.data.mass !== undefined) ? node.data.mass : 1.0;
+ this.nodePoints[node.id] = new Layout.ForceDirected.Point(Vector.random(), mass);
+ }
+
+ return this.nodePoints[node.id];
+ };
+
+ Layout.ForceDirected.prototype.spring = function(edge) {
+ if (!(edge.id in this.edgeSprings)) {
+ var length = (edge.data.length !== undefined) ? edge.data.length : 1.0;
+
+ var existingSpring = false;
+
+ var from = this.graph.getEdges(edge.source, edge.target);
+ from.forEach(function(e) {
+ if (existingSpring === false && e.id in this.edgeSprings) {
+ existingSpring = this.edgeSprings[e.id];
+ }
+ }, this);
+
+ if (existingSpring !== false) {
+ return new Layout.ForceDirected.Spring(existingSpring.point1, existingSpring.point2, 0.0, 0.0);
+ }
+
+ var to = this.graph.getEdges(edge.target, edge.source);
+ from.forEach(function(e){
+ if (existingSpring === false && e.id in this.edgeSprings) {
+ existingSpring = this.edgeSprings[e.id];
+ }
+ }, this);
+
+ if (existingSpring !== false) {
+ return new Layout.ForceDirected.Spring(existingSpring.point2, existingSpring.point1, 0.0, 0.0);
+ }
+
+ this.edgeSprings[edge.id] = new Layout.ForceDirected.Spring(
+ this.point(edge.source), this.point(edge.target), length, this.stiffness
+ );
+ }
+
+ return this.edgeSprings[edge.id];
+ };
+
+ // callback should accept two arguments: Node, Point
+ Layout.ForceDirected.prototype.eachNode = function(callback) {
+ var t = this;
+ this.graph.nodes.forEach(function(n){
+ callback.call(t, n, t.point(n));
+ });
+ };
+
+ // callback should accept two arguments: Edge, Spring
+ Layout.ForceDirected.prototype.eachEdge = function(callback) {
+ var t = this;
+ this.graph.edges.forEach(function(e){
+ callback.call(t, e, t.spring(e));
+ });
+ };
+
+ // callback should accept one argument: Spring
+ Layout.ForceDirected.prototype.eachSpring = function(callback) {
+ var t = this;
+ this.graph.edges.forEach(function(e){
+ callback.call(t, t.spring(e));
+ });
+ };
+
+
+ // Physics stuff
+ Layout.ForceDirected.prototype.applyCoulombsLaw = function() {
+ this.eachNode(function(n1, point1) {
+ this.eachNode(function(n2, point2) {
+ if (point1 !== point2)
+ {
+ var d = point1.p.subtract(point2.p);
+ var distance = d.magnitude() + 0.1; // avoid massive forces at small distances (and divide by zero)
+ var direction = d.normalise();
+
+ // apply force to each end point
+ point1.applyForce(direction.multiply(this.repulsion).divide(distance * distance * 0.5));
+ point2.applyForce(direction.multiply(this.repulsion).divide(distance * distance * -0.5));
+ }
+ });
+ });
+ };
+
+ Layout.ForceDirected.prototype.applyHookesLaw = function() {
+ this.eachSpring(function(spring){
+ var d = spring.point2.p.subtract(spring.point1.p); // the direction of the spring
+ var displacement = spring.length - d.magnitude();
+ var direction = d.normalise();
+
+ // apply force to each end point
+ spring.point1.applyForce(direction.multiply(spring.k * displacement * -0.5));
+ spring.point2.applyForce(direction.multiply(spring.k * displacement * 0.5));
+ });
+ };
+
+ Layout.ForceDirected.prototype.attractToCentre = function() {
+ this.eachNode(function(node, point) {
+ var direction = point.p.multiply(-1.0);
+ point.applyForce(direction.multiply(this.repulsion / 50.0));
+ });
+ };
+
+
+ Layout.ForceDirected.prototype.updateVelocity = function(timestep) {
+ this.eachNode(function(node, point) {
+ // Is this, along with updatePosition below, the only places that your
+ // integration code exist?
+ point.v = point.v.add(point.a.multiply(timestep)).multiply(this.damping);
+ if (point.v.magnitude() > this.maxSpeed) {
+ point.v = point.v.normalise().multiply(this.maxSpeed);
+ }
+ point.a = new Vector(0,0);
+ });
+ };
+
+ Layout.ForceDirected.prototype.updatePosition = function(timestep) {
+ this.eachNode(function(node, point) {
+ // Same question as above; along with updateVelocity, is this all of
+ // your integration code?
+ point.p = point.p.add(point.v.multiply(timestep));
+ });
+ };
+
+ // Calculate the total kinetic energy of the system
+ Layout.ForceDirected.prototype.totalEnergy = function(timestep) {
+ var energy = 0.0;
+ this.eachNode(function(node, point) {
+ var speed = point.v.magnitude();
+ energy += 0.5 * point.m * speed * speed;
+ });
+
+ return energy;
+ };
+
+ var __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; // stolen from coffeescript, thanks jashkenas! ;-)
+
+ Springy.requestAnimationFrame = __bind(this.requestAnimationFrame ||
+ this.webkitRequestAnimationFrame ||
+ this.mozRequestAnimationFrame ||
+ this.oRequestAnimationFrame ||
+ this.msRequestAnimationFrame ||
+ (function(callback, element) {
+ this.setTimeout(callback, 10);
+ }), this);
+
+
+ /**
+ * Start simulation if it's not running already.
+ * In case it's running then the call is ignored, and none of the callbacks passed is ever executed.
+ */
+ Layout.ForceDirected.prototype.start = function(render, onRenderStop, onRenderStart) {
+ var t = this;
+
+ if (this._started) return;
+ this._started = true;
+ this._stop = false;
+
+ if (onRenderStart !== undefined) { onRenderStart(); }
+
+ Springy.requestAnimationFrame(function step() {
+ t.tick(0.03);
+
+ if (render !== undefined) {
+ render();
+ }
+
+ // stop simulation when energy of the system goes below a threshold
+ if (t._stop || t.totalEnergy() < t.minEnergyThreshold) {
+ t._started = false;
+ if (onRenderStop !== undefined) { onRenderStop(); }
+ } else {
+ Springy.requestAnimationFrame(step);
+ }
+ });
+ };
+
+ Layout.ForceDirected.prototype.stop = function() {
+ this._stop = true;
+ }
+
+ Layout.ForceDirected.prototype.tick = function(timestep) {
+ this.applyCoulombsLaw();
+ this.applyHookesLaw();
+ this.attractToCentre();
+ this.updateVelocity(timestep);
+ this.updatePosition(timestep);
+ };
+
+ // Find the nearest point to a particular position
+ Layout.ForceDirected.prototype.nearest = function(pos) {
+ var min = {node: null, point: null, distance: null};
+ var t = this;
+ this.graph.nodes.forEach(function(n){
+ var point = t.point(n);
+ var distance = point.p.subtract(pos).magnitude();
+
+ if (min.distance === null || distance < min.distance) {
+ min = {node: n, point: point, distance: distance};
+ }
+ });
+
+ return min;
+ };
+
+ // returns [bottomleft, topright]
+ Layout.ForceDirected.prototype.getBoundingBox = function() {
+ var bottomleft = new Vector(-2,-2);
+ var topright = new Vector(2,2);
+
+ this.eachNode(function(n, point) {
+ if (point.p.x < bottomleft.x) {
+ bottomleft.x = point.p.x;
+ }
+ if (point.p.y < bottomleft.y) {
+ bottomleft.y = point.p.y;
+ }
+ if (point.p.x > topright.x) {
+ topright.x = point.p.x;
+ }
+ if (point.p.y > topright.y) {
+ topright.y = point.p.y;
+ }
+ });
+
+ var padding = topright.subtract(bottomleft).multiply(0.07); // ~5% padding
+
+ return {bottomleft: bottomleft.subtract(padding), topright: topright.add(padding)};
+ };
+
+
+ // Vector
+ var Vector = Springy.Vector = function(x, y) {
+ this.x = x;
+ this.y = y;
+ };
+
+ Vector.random = function() {
+ return new Vector(10.0 * (Math.random() - 0.5), 10.0 * (Math.random() - 0.5));
+ };
+
+ Vector.prototype.add = function(v2) {
+ return new Vector(this.x + v2.x, this.y + v2.y);
+ };
+
+ Vector.prototype.subtract = function(v2) {
+ return new Vector(this.x - v2.x, this.y - v2.y);
+ };
+
+ Vector.prototype.multiply = function(n) {
+ return new Vector(this.x * n, this.y * n);
+ };
+
+ Vector.prototype.divide = function(n) {
+ return new Vector((this.x / n) || 0, (this.y / n) || 0); // Avoid divide by zero errors..
+ };
+
+ Vector.prototype.magnitude = function() {
+ return Math.sqrt(this.x*this.x + this.y*this.y);
+ };
+
+ Vector.prototype.normal = function() {
+ return new Vector(-this.y, this.x);
+ };
+
+ Vector.prototype.normalise = function() {
+ return this.divide(this.magnitude());
+ };
+
+ // Point
+ Layout.ForceDirected.Point = function(position, mass) {
+ this.p = position; // position
+ this.m = mass; // mass
+ this.v = new Vector(0, 0); // velocity
+ this.a = new Vector(0, 0); // acceleration
+ };
+
+ Layout.ForceDirected.Point.prototype.applyForce = function(force) {
+ this.a = this.a.add(force.divide(this.m));
+ };
+
+ // Spring
+ Layout.ForceDirected.Spring = function(point1, point2, length, k) {
+ this.point1 = point1;
+ this.point2 = point2;
+ this.length = length; // spring length at rest
+ this.k = k; // spring constant (See Hooke's law) .. how stiff the spring is
+ };
+
+ // Layout.ForceDirected.Spring.prototype.distanceToPoint = function(point)
+ // {
+ // // hardcore vector arithmetic.. ohh yeah!
+ // // .. see http://stackoverflow.com/questions/849211/shortest-distance-between-a-point-and-a-line-segment/865080#865080
+ // var n = this.point2.p.subtract(this.point1.p).normalise().normal();
+ // var ac = point.p.subtract(this.point1.p);
+ // return Math.abs(ac.x * n.x + ac.y * n.y);
+ // };
+
+ /**
+ * Renderer handles the layout rendering loop
+ * @param onRenderStop optional callback function that gets executed whenever rendering stops.
+ * @param onRenderStart optional callback function that gets executed whenever rendering starts.
+ * @param onRenderFrame optional callback function that gets executed after each frame is rendered.
+ */
+ var Renderer = Springy.Renderer = function(layout, clear, drawEdge, drawNode, onRenderStop, onRenderStart, onRenderFrame) {
+ this.layout = layout;
+ this.clear = clear;
+ this.drawEdge = drawEdge;
+ this.drawNode = drawNode;
+ this.onRenderStop = onRenderStop;
+ this.onRenderStart = onRenderStart;
+ this.onRenderFrame = onRenderFrame;
+
+ this.layout.graph.addGraphListener(this);
+ }
+
+ Renderer.prototype.graphChanged = function(e) {
+ this.start();
+ };
+
+ /**
+ * Starts the simulation of the layout in use.
+ *
+ * Note that in case the algorithm is still or already running then the layout that's in use
+ * might silently ignore the call, and your optional done
callback is never executed.
+ * At least the built-in ForceDirected layout behaves in this way.
+ *
+ * @param done An optional callback function that gets executed when the springy algorithm stops,
+ * either because it ended or because stop() was called.
+ */
+ Renderer.prototype.start = function(done) {
+ var t = this;
+ this.layout.start(function render() {
+ t.clear();
+
+ t.layout.eachEdge(function(edge, spring) {
+ t.drawEdge(edge, spring.point1.p, spring.point2.p);
+ });
+
+ t.layout.eachNode(function(node, point) {
+ t.drawNode(node, point.p);
+ });
+
+ if (t.onRenderFrame !== undefined) { t.onRenderFrame(); }
+ }, this.onRenderStop, this.onRenderStart);
+ };
+
+ Renderer.prototype.stop = function() {
+ this.layout.stop();
+ };
+
+ // Array.forEach implementation for IE support..
+ //https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/forEach
+ if ( !Array.prototype.forEach ) {
+ Array.prototype.forEach = function( callback, thisArg ) {
+ var T, k;
+ if ( this == null ) {
+ throw new TypeError( " this is null or not defined" );
+ }
+ var O = Object(this);
+ var len = O.length >>> 0; // Hack to convert O.length to a UInt32
+ if ( {}.toString.call(callback) != "[object Function]" ) {
+ throw new TypeError( callback + " is not a function" );
+ }
+ if ( thisArg ) {
+ T = thisArg;
+ }
+ k = 0;
+ while( k < len ) {
+ var kValue;
+ if ( k in O ) {
+ kValue = O[ k ];
+ callback.call( T, kValue, k, O );
+ }
+ k++;
+ }
+ };
+ }
+
+ var isEmpty = function(obj) {
+ for (var k in obj) {
+ if (obj.hasOwnProperty(k)) {
+ return false;
+ }
+ }
+ return true;
+ };
+
+ return Springy;
+}));
diff --git a/test/springy/springyui.js b/test/springy/springyui.js
new file mode 100755
index 0000000..acc35eb
--- /dev/null
+++ b/test/springy/springyui.js
@@ -0,0 +1,394 @@
+/**
+Copyright (c) 2010 Dennis Hotson
+
+ Permission is hereby granted, free of charge, to any person
+ obtaining a copy of this software and associated documentation
+ files (the "Software"), to deal in the Software without
+ restriction, including without limitation the rights to use,
+ copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the
+ Software is furnished to do so, subject to the following
+ conditions:
+
+ The above copyright notice and this permission notice shall be
+ included in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ OTHER DEALINGS IN THE SOFTWARE.
+*/
+
+(function() {
+
+jQuery.fn.springy = function(params) {
+ var graph = this.graph = params.graph || new Springy.Graph();
+ var nodeFont = "16px Verdana, sans-serif";
+ var edgeFont = "8px Verdana, sans-serif";
+ var stiffness = params.stiffness || 400.0;
+ var repulsion = params.repulsion || 400.0;
+ var damping = params.damping || 0.5;
+ var minEnergyThreshold = params.minEnergyThreshold || 0.00001;
+ var nodeSelected = params.nodeSelected || null;
+ var nodeImages = {};
+ var edgeLabelsUpright = true;
+
+ var canvas = this[0];
+ var ctx = canvas.getContext("2d");
+
+ var layout = this.layout = new Springy.Layout.ForceDirected(graph, stiffness, repulsion, damping, minEnergyThreshold);
+
+ // calculate bounding box of graph layout.. with ease-in
+ var currentBB = layout.getBoundingBox();
+ var targetBB = {bottomleft: new Springy.Vector(-2, -2), topright: new Springy.Vector(2, 2)};
+
+ // auto adjusting bounding box
+ Springy.requestAnimationFrame(function adjust() {
+ targetBB = layout.getBoundingBox();
+ // current gets 20% closer to target every iteration
+ currentBB = {
+ bottomleft: currentBB.bottomleft.add( targetBB.bottomleft.subtract(currentBB.bottomleft)
+ .divide(10)),
+ topright: currentBB.topright.add( targetBB.topright.subtract(currentBB.topright)
+ .divide(10))
+ };
+
+ Springy.requestAnimationFrame(adjust);
+ });
+
+ // convert to/from screen coordinates
+ var toScreen = function(p) {
+ var size = currentBB.topright.subtract(currentBB.bottomleft);
+ var sx = p.subtract(currentBB.bottomleft).divide(size.x).x * canvas.width;
+ var sy = p.subtract(currentBB.bottomleft).divide(size.y).y * canvas.height;
+ return new Springy.Vector(sx, sy);
+ };
+
+ var fromScreen = function(s) {
+ var size = currentBB.topright.subtract(currentBB.bottomleft);
+ var px = (s.x / canvas.width) * size.x + currentBB.bottomleft.x;
+ var py = (s.y / canvas.height) * size.y + currentBB.bottomleft.y;
+ return new Springy.Vector(px, py);
+ };
+
+ // half-assed drag and drop
+ var selected = null;
+ var nearest = null;
+ var dragged = null;
+
+ jQuery(canvas).mousedown(function(e) {
+ var pos = jQuery(this).offset();
+ var p = fromScreen({x: e.pageX - pos.left, y: e.pageY - pos.top});
+ selected = nearest = dragged = layout.nearest(p);
+
+ if (selected.node !== null) {
+ dragged.point.m = 10000.0;
+
+ if (nodeSelected) {
+ nodeSelected(selected.node);
+ }
+ }
+
+ renderer.start();
+ });
+
+ // Basic double click handler
+ jQuery(canvas).dblclick(function(e) {
+ var pos = jQuery(this).offset();
+ var p = fromScreen({x: e.pageX - pos.left, y: e.pageY - pos.top});
+ selected = layout.nearest(p);
+ node = selected.node;
+ if (node && node.data && node.data.ondoubleclick) {
+ node.data.ondoubleclick();
+ }
+ });
+
+ jQuery(canvas).mousemove(function(e) {
+ var pos = jQuery(this).offset();
+ var p = fromScreen({x: e.pageX - pos.left, y: e.pageY - pos.top});
+ nearest = layout.nearest(p);
+
+ if (dragged !== null && dragged.node !== null) {
+ dragged.point.p.x = p.x;
+ dragged.point.p.y = p.y;
+ }
+
+ renderer.start();
+ });
+
+ jQuery(window).bind('mouseup',function(e) {
+ dragged = null;
+ });
+
+ var getTextWidth = function(node) {
+ var text = (node.data.label !== undefined) ? node.data.label : node.id;
+ if (node._width && node._width[text])
+ return node._width[text];
+
+ ctx.save();
+ ctx.font = (node.data.font !== undefined) ? node.data.font : nodeFont;
+ var width = ctx.measureText(text).width;
+ ctx.restore();
+
+ node._width || (node._width = {});
+ node._width[text] = width;
+
+ return width;
+ };
+
+ var getTextHeight = function(node) {
+ return 16;
+ // In a more modular world, this would actually read the font size, but I think leaving it a constant is sufficient for now.
+ // If you change the font size, I'd adjust this too.
+ };
+
+ var getImageWidth = function(node) {
+ var width = (node.data.image.width !== undefined) ? node.data.image.width : nodeImages[node.data.image.src].object.width;
+ return width;
+ }
+
+ var getImageHeight = function(node) {
+ var height = (node.data.image.height !== undefined) ? node.data.image.height : nodeImages[node.data.image.src].object.height;
+ return height;
+ }
+
+ Springy.Node.prototype.getHeight = function() {
+ var height;
+ if (this.data.image == undefined) {
+ height = getTextHeight(this);
+ } else {
+ if (this.data.image.src in nodeImages && nodeImages[this.data.image.src].loaded) {
+ height = getImageHeight(this);
+ } else {height = 10;}
+ }
+ return height;
+ }
+
+ Springy.Node.prototype.getWidth = function() {
+ var width;
+ if (this.data.image == undefined) {
+ width = getTextWidth(this);
+ } else {
+ if (this.data.image.src in nodeImages && nodeImages[this.data.image.src].loaded) {
+ width = getImageWidth(this);
+ } else {width = 10;}
+ }
+ return width;
+ }
+
+ var renderer = this.renderer = new Springy.Renderer(layout,
+ function clear() {
+ ctx.clearRect(0,0,canvas.width,canvas.height);
+ },
+ function drawEdge(edge, p1, p2) {
+ var x1 = toScreen(p1).x;
+ var y1 = toScreen(p1).y;
+ var x2 = toScreen(p2).x;
+ var y2 = toScreen(p2).y;
+
+ var direction = new Springy.Vector(x2-x1, y2-y1);
+ var normal = direction.normal().normalise();
+
+ var from = graph.getEdges(edge.source, edge.target);
+ var to = graph.getEdges(edge.target, edge.source);
+
+ var total = from.length + to.length;
+
+ // Figure out edge's position in relation to other edges between the same nodes
+ var n = 0;
+ for (var i=0; i Math.PI/2 || angle < -Math.PI/2)) {
+ displacement = 8;
+ angle += Math.PI;
+ }
+ var textPos = s1.add(s2).divide(2).add(normal.multiply(displacement));
+ ctx.translate(textPos.x, textPos.y);
+ ctx.rotate(angle);
+ ctx.fillText(text, 0,-2);
+ ctx.restore();
+ }
+
+ },
+ function drawNode(node, p) {
+ var s = toScreen(p);
+
+ ctx.save();
+
+ // Pulled out the padding aspect sso that the size functions could be used in multiple places
+ // These should probably be settable by the user (and scoped higher) but this suffices for now
+ var paddingX = 6;
+ var paddingY = 6;
+
+ var contentWidth = node.getWidth();
+ var contentHeight = node.getHeight();
+ var boxWidth = contentWidth + paddingX;
+ var boxHeight = contentHeight + paddingY;
+
+ // clear background
+ ctx.clearRect(s.x - boxWidth/2, s.y - boxHeight/2, boxWidth, boxHeight);
+
+ // fill background
+ if (selected !== null && selected.node !== null && selected.node.id === node.id) {
+ ctx.fillStyle = "#FFFFE0";
+ } else if (nearest !== null && nearest.node !== null && nearest.node.id === node.id) {
+ ctx.fillStyle = "#EEEEEE";
+ } else {
+ ctx.fillStyle = "#FFFFFF";
+ }
+ ctx.fillRect(s.x - boxWidth/2, s.y - boxHeight/2, boxWidth, boxHeight);
+
+ if (node.data.image == undefined) {
+ ctx.textAlign = "left";
+ ctx.textBaseline = "top";
+ ctx.font = (node.data.font !== undefined) ? node.data.font : nodeFont;
+ ctx.fillStyle = (node.data.color !== undefined) ? node.data.color : "#000000";
+ var text = (node.data.label !== undefined) ? node.data.label : node.id;
+ ctx.fillText(text, s.x - contentWidth/2, s.y - contentHeight/2);
+ } else {
+ // Currently we just ignore any labels if the image object is set. One might want to extend this logic to allow for both, or other composite nodes.
+ var src = node.data.image.src; // There should probably be a sanity check here too, but un-src-ed images aren't exaclty a disaster.
+ if (src in nodeImages) {
+ if (nodeImages[src].loaded) {
+ // Our image is loaded, so it's safe to draw
+ ctx.drawImage(nodeImages[src].object, s.x - contentWidth/2, s.y - contentHeight/2, contentWidth, contentHeight);
+ }
+ }else{
+ // First time seeing an image with this src address, so add it to our set of image objects
+ // Note: we index images by their src to avoid making too many duplicates
+ nodeImages[src] = {};
+ var img = new Image();
+ nodeImages[src].object = img;
+ img.addEventListener("load", function () {
+ // HTMLImageElement objects are very finicky about being used before they are loaded, so we set a flag when it is done
+ nodeImages[src].loaded = true;
+ });
+ img.src = src;
+ }
+ }
+ ctx.restore();
+ }
+ );
+
+ renderer.start();
+
+ // helpers for figuring out where to draw arrows
+ function intersect_line_line(p1, p2, p3, p4) {
+ var denom = ((p4.y - p3.y)*(p2.x - p1.x) - (p4.x - p3.x)*(p2.y - p1.y));
+
+ // lines are parallel
+ if (denom === 0) {
+ return false;
+ }
+
+ var ua = ((p4.x - p3.x)*(p1.y - p3.y) - (p4.y - p3.y)*(p1.x - p3.x)) / denom;
+ var ub = ((p2.x - p1.x)*(p1.y - p3.y) - (p2.y - p1.y)*(p1.x - p3.x)) / denom;
+
+ if (ua < 0 || ua > 1 || ub < 0 || ub > 1) {
+ return false;
+ }
+
+ return new Springy.Vector(p1.x + ua * (p2.x - p1.x), p1.y + ua * (p2.y - p1.y));
+ }
+
+ function intersect_line_box(p1, p2, p3, w, h) {
+ var tl = {x: p3.x, y: p3.y};
+ var tr = {x: p3.x + w, y: p3.y};
+ var bl = {x: p3.x, y: p3.y + h};
+ var br = {x: p3.x + w, y: p3.y + h};
+
+ var result;
+ if (result = intersect_line_line(p1, p2, tl, tr)) { return result; } // top
+ if (result = intersect_line_line(p1, p2, tr, br)) { return result; } // right
+ if (result = intersect_line_line(p1, p2, br, bl)) { return result; } // bottom
+ if (result = intersect_line_line(p1, p2, bl, tl)) { return result; } // left
+
+ return false;
+ }
+
+ return this;
+}
+
+})();
diff --git a/test/tail/tail.txt b/test/tail/tail.txt
new file mode 100644
index 0000000..3eaf408
--- /dev/null
+++ b/test/tail/tail.txt
@@ -0,0 +1,287 @@
+$totalCorrect / 1 correct
";
+ //The halting problem
+ foreach($_POST as $variable => $value) {
+ $value = str_replace(' ', '_', $value);
+ // checking whether file exists or not
+ $file_pointer = "./en/" . $value . ".html";
+ if (file_exists($file_pointer))
+ {
+ echo "The file $file_pointer already exists