This is a simulator Matt Woelk wrote for an Agent Based Modelling Course in 2013. The paper for this work is located here.
xxxxxxxxxx
<!--{{{-->
<html>
<head>
<title>Morphogenesis</title>
<meta charset="utf-8">
<script type="text/javascript" src="readme-d3_modded.js"></script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.4.4/underscore-min.js"></script>
<link type="text/css" rel="stylesheet" href="readme-voronoi.css"/>
</head>
<body>
<input id='zoomin' type='button' onclick='splitAllNodesA_AA()' value = 'A --> A, A' />
<input id='zoomin' type='button' onclick='splitAllNodesB_BA()' value = 'B --> B, A' />
<input id='plause' type='button' style="width:5em" onclick='stop()' value='pause' />
<!--<input id='generating' type='button' style="width:9em" onclick='stopGen()' value='stop generating' />-->
<input id='toggleMouseHighlighting' type='button' style="width:15em" onclick='toggleMouseHighlighting()' value='remove highlights' />
</br>
<!--<li><label><input type="radio" name="casting" value="multi"/>Multi Cast</li>-->
<!--<li><label><input type="radio" name="casting" value="mono"/>Mono Cast</li>-->
<form id="casting">
<label><input type="radio" name="casting" value="monoToEdge" checked="checked"/>Random Walk Until Reaching An Edge Cell
<!--<li><label><input type="radio" name="casting" value="monoToEdgeWithSpine" checked="checked"/>Walk to edge, then make head and spine</li>-->
<label><input type="radio" name="casting" value="automation" checked="checked"/>Start Automation (click on edge cell) <input id='step' type='button' style="width:4em" onclick='automateIteration()' value='iterate' />
</form>
</br>
<script type="text/javascript">
//{{{ VARIABLES
var generating = false;
var generationPeriod = 500;
var paused = false;
var showingMouseHighlights = false;
var startingQuantity = 0; //6 is best
var headParent = 0;
//document.getElementById('generating').value = generating ? 'stop generating' : 'start simulation';
document.getElementById('plause').value = paused ? 'play' : 'pause';
document.getElementById('toggleMouseHighlighting').value = showingMouseHighlights ? 'remove mouse highlighting' : 'engage mouse highlighting';
var w = 860;
var h = 430;
var fill = d3.scale.category10();
var nodes = []; // this is treated like a global variable
//var links = []; // "global" variable, links between the nodes, by index.
var foci = {x: w/2, y: h/2};
var cellRadius = 34; //34 for charge strength of -30, distance of 20, and gravity 0.05
var nucleusradius = 7;
var vis = d3.select("body").append("svg:svg")
.attr("width", w)
.attr("height", h)
.attr("class", "PiYG");
//defaults: size 1×1
// friction 0.9
// distance 20
// charge strength -30
// gravity strength 0.1
// and theta parameter 0.8
var force = d3.layout.force()
.nodes(nodes)
.links([])
.friction(0.85)
.gravity(0.05)
.size([w, h]);
var clips = vis.append("svg:clipPath");
// usage: colors[type/state]
var colors = {
"normal" : "#aaa", // grey,
"head" : "#000", // black,
"spine" : "#FC8D62", // orangish,
"right" : "#8DA0CB", // blueish,
"left" : "#66C2A5", // greenish,
"indicator" : "#ddd",
"deselected" : "#000",
"interest" : "#000",
"selected" : "#F00",
}
// VARIABLES }}}
//{{{ TICK
force.on("tick", function(e) {
// Push different nodes in different directions for clustering.
//var k = 0.010 * e.alpha;
//nodes.forEach(function(o, i) {
// if (o.type === "head") {
// if (i === headParent) {
// //console.log("unconsolable");
// }
// var distx = 20;
// var disty = 20;
// var distx = nodes[headParent].x - o.x;
// var disty = nodes[headParent].y - o.y;
// o.y += disty * k;
// o.x += distx * k;
// }
//});
// ENTER voronoi //
vis.selectAll("path")
.data(d3.geom.voronoi(_.map(nodes, function (d) { return [d.x, d.y]; })))
.enter().append("path")
.attr("clip-path", function (d, i) { return "url(#clip"+i+")"; })
.attr("class", function(d, i) { return i ? "q" + (i % 9) + "-9" : null; })
.on("mouseover", function (d, i) {
if (showingMouseHighlights) {
return highlightNeighboursIncludingSelf(i);
} else {
return;
}})
.on("click", function (d, i) { return cloock(i); })
.style("stroke", "#aaa")
.attr("d", function(d) { return "M" + d.join("L") + "Z"; });
// UPDATE voronoi //
vis.selectAll("path")
.data(d3.geom.voronoi(_.map(nodes, function (d) { return [d.x, d.y]; }))
.map(function(d) { return "M" + d.join("L") + "Z"; }))
.filter(function(d) { return this.getAttribute("d") != d; })
.attr("d", function(d) { return d; });
// ENTER circles //
vis.selectAll("circle.node")
.data(nodes)
.enter().append("svg:circle")
.attr("class", "node")
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
.attr("r", nucleusradius)
.style("stroke", function(d) { return colors[d.state]; })
.style("stroke-width", 1.5)
.style("fill", function(d) { return colors[d.type]; })
.call(force.drag);
// UPDATE circles //
vis.selectAll("circle.node")
.attr("cx", function (d) { return d.x; })
.attr("cy", function (d) { return d.y; })
.attr("stroke", function (d) {
return colors[d.state];
})
.attr("fill", function (d) {
return colors[d.type];
});
// DELETE AND THEN ENTER clipping regions //
vis.selectAll(".clipper")
.remove();
vis.selectAll(".clipper")
.data(nodes)
.enter().append("svg:clipPath")
.attr("id", function (d, i) { return "clip"+i; })
.attr("class", "clipper")
.append("svg:circle")
.attr('cx', function (d) { return d.x; })
.attr('cy', function (d) { return d.y; })
.attr('r', cellRadius);
});
// TICK }}}
//{{{ INITIAL SETUP
vis.selectAll("path")
.data(d3.geom.voronoi(_.map(nodes, function (d) { return [d.x, d.y]; })))
.enter().append("path")
.attr("class", function(d, i) { return i ? "q" + (i % 9) + "-9" : null; })
.attr("d", function(d) { return "M" + d.join("L") + "Z"; });
var incrementer = -1
// set up interval of generation
setInterval('addNodeIfGen', generationPeriod);
console.log("===Simulation Started===");
// INITIAL SETUP }}}
//{{{ INTERFACE
function cloock (index) {
var b = document.querySelector("#casting input:checked").value;
if (b.localeCompare("mono") === 0) {
propagateOneToOne(index);
} else if (b.localeCompare("multi") === 0){
propagateOneToAll(index);
} else if (b.localeCompare("monoToEdge") === 0){
propagateMonoToEdge(index, true, 3);
} else if (b.localeCompare("monoToEdgeWithSpine") === 0){
propagateMonoToEdgeWithSpine(index, true, 3);
} else if (b.localeCompare("automation") === 0){
startAutomation(index);
} else {
console.log("UNIMPLEMENTED")
}
}
var stopGen = function () {
if (generating === false) {
generating = true;
//document.getElementById('generating').value = "stop simulation";
}else{
generating = false;
//document.getElementById('generating').value = "start simulation";
}
}
var stop = function () {
if (paused === false) {
paused = true;
force.stop();
document.getElementById('plause').value = "play";
}else{
paused = false;
force.start();
document.getElementById('plause').value = "pause";
}
}
// INTERFACE }}}
//{{{ CHANGING STATE
function updateColors () {
vis.selectAll("circle.node")
.style("stroke", function(d) {
return colors[d.state];
})
.style("fill", function(d) {
return colors[d.type];
});
}
function highlightSelf (nodeOfInterestIndex) {
nodes[nodeOfInterestIndex].type = "head";
updateColors();
}
function highlightSelfState (nodeOfInterestIndex) {
nodes[nodeOfInterestIndex].state = "selected";
updateColors();
}
function highlightNeighboursIncludingSelf (nodeOfInterestIndex) {
var index = nodeOfInterestIndex;
removeHighlights();
highlightSelfState(nodeOfInterestIndex);
highlightNeighbours(nodeOfInterestIndex);
updateColors();
}
function toggleMouseHighlighting () {
showingMouseHighlights = showingMouseHighlights ? false : true;
document.getElementById('toggleMouseHighlighting').value = showingMouseHighlights ? 'remove mouse highlighting' : 'engage mouse highlighting';
if (!showingMouseHighlights) {
removeHighlights();
}
}
function removeHighlights () {
_.each(nodes, function (d, i) {
//if (d.state === "selected") {
nodes[i].state = "deselected";
//}
});
updateColors();
}
function highlightNeighbours (nodeOfInterestIndex) {
var neighbours = generateAllNeighbours()[nodeOfInterestIndex];
_.each(neighbours, function (i) {
nodes[i].state = "selected";
});
updateColors();
}
// CHANGING STATE }}}
//{{{ PROPAGATING
function propagateOneToAll (index) {
var neighbours = generateAllNeighbours()[index];
var nbs = _.filter(neighbours, function (d) { return nodes[d].state.localeCompare("deselected") === 0 || nodes[d].state.localeCompare("selected") === 0; });
highlightSelf(index);
if (!paused && nbs.length > 0) {
_.forEach(nbs, function (d,i) {
setTimeout(propagateOneToAll, generationPeriod, d);
});
}
}
function propagateOneToOne (index) {
var neighbours = generateAllNeighbours()[index];
var nbs = _.filter(neighbours, function (d) { return nodes[d].state.localeCompare("deselected") === 0 || nodes[d].state.localeCompare("selected") === 0; });
var rndm = _.random(0, nbs.length - 1);
highlightSelf(index);
if (!paused && nbs.length > 0) {
setTimeout(propagateOneToOne, generationPeriod, [nbs[rndm]]);
}
}
function propagateMonoToEdgeWithSpine (index, startGrowingThere, howManyToGrow) {
var neighbours = generateAllNeighbours()[index];
if (checkIfEdge(index)) {
nodes[index].type = "head";
updateColors();
headParent = index;
//nodes[headParent].fixed = true;
if (startGrowingThere) {
startAutomation(index);
splitNodeAmountOfTimesHead(index, howManyToGrow, "head");
//splitNodeAmountOfTimesHead(index, howManyToGrow, "spine");
}
} else if (!paused && neighbours.length > 0) {
var rndm = _.random(0, neighbours.length - 1);
highlightSelf(index);
setTimeout(propagateMonoToEdge, generationPeriod, neighbours[rndm], startGrowingThere, howManyToGrow);
}
}
function propagateMonoToEdge (index, startGrowingThere, howManyToGrow) {
var neighbours = generateAllNeighbours()[index];
if (checkIfEdge(index)) {
nodes[index].type = "head";
updateColors();
headParent = index;
//nodes[headParent].fixed = true;
if (startGrowingThere) {
splitNodeAmountOfTimesHead(index, howManyToGrow, "head");
//splitNodeAmountOfTimesHead(index, howManyToGrow, "spine");
}
} else if (!paused && neighbours.length > 0) {
var rndm = _.random(0, neighbours.length - 1);
highlightSelfState(index);
updateColors();
setTimeout(propagateMonoToEdge, generationPeriod, neighbours[rndm], startGrowingThere, howManyToGrow);
}
}
function automateIteration() {
var allNeighbours = generateAllNeighbours();
for (nod in nodes) {
if (nodes[nod].type.localeCompare("head") === 0) { continue; } // don't affect head nodes
var r = _.some(allNeighbours[nod], function (d) {
return nodes[d].type.localeCompare("right") === 0;
}); // if it has a right neighbour
var l = _.some(allNeighbours[nod], function (d) {
return nodes[d].type.localeCompare("left") === 0;
}); // if it has a left neighbour
if (r && l) {
/*it is touching right and left*/
nodes[nod].type = "spine";
} else if (r) {
nodes[nod].type = "right";//make it right
} else if (l) {
nodes[nod].type = "left";//make it left
}
}
updateColors();
}
function startAutomation(index) {
var neighbours = generateAllNeighbours()[index];
var edgeNeighbours = _.filter(neighbours, function (d) {
return checkIfEdge(d);
});
if(edgeNeighbours.length < 2) {
alert("Please select a cell with more than one edge cell neighbour");
return;
}
nodes[edgeNeighbours[0]].type = "left";
nodes[edgeNeighbours[1]].type = "right";
automateIteration();
}
// PROPAGATING }}}
//{{{ ADDING AND REMOVING NODES
function copyLinks(ln) {
var lnew = []
_.each(ln, function (l) {
lnew.push({source:l.source, target:l.target});
});
return lnew;
}
var addNode = function (indexOfNodeToMultiply, type, state) {
if (!state) {
var state = "deselected";
}
if (paused === false){
var rad = Math.random() * Math.PI * 2;
var xoffset = Math.cos(rad) * nucleusradius;
var yoffset = Math.sin(rad) * nucleusradius;
// spawn a new node nucleusradius away from the previous node in a random direction.
nodes.push({ // push a new node at the location of the previous one
x: (incrementer === -1) ? foci.x : nodes[indexOfNodeToMultiply].x + xoffset,
y: (incrementer === -1) ? foci.y : nodes[indexOfNodeToMultiply].y + yoffset,
type: type,
state: state,
}); //{id: ~~(Math.random())});
incrementer++;
force.start();
}
};
var splitNodeAmountOfTimesHead = function (indexOfNodeToMultiply, howManyTimes, type) {
var i = howManyTimes;
//nodes[indexOfNodeToMultiply].type = "head";
while (i >= 0) {
setTimeout(addNode, generationPeriod * i / 2, indexOfNodeToMultiply, type);
i = i - 1;
}
}
function addLink(nodChild, nodParent) {
//links.push({source: nodChild,
//target: nodParent});
//force.links(copyLinks(links));
//force.start();
}
function splitAllOfTypeWithLink (type) {
_.each(nodes, function (nod, i) {
if (nod.type === type) {
addNode(i, "normal");
nodes[nodes.length - 1].type = type;
addLink(i, nodes.length - 1);
}
});
}
var splitNodeAmountOfTimes = function (indexOfNodeToMultiply, howManyTimes) {
var i = howManyTimes;
while (i >= 0) {
setTimeout(addNode, generationPeriod * i / 2, indexOfNodeToMultiply, "normal");
i = i - 1;
}
}
var addNewNodeToEndOfList = function () {
addNode(incrementer, "normal");
}
var splitAllNodesA_AA = function () {
// split every node into two nodes
var i = 0;
incNow = incrementer + 1;
//for every node, make a new node
for (i = 0; i < incNow; i++) {
addNode(i, nodes[i].type);
}
};
var splitAllNodesB_BA = function () {
// split every node into two nodes
var i = 0;
incNow = incrementer + 1;
//for every node, make a new node
for (i = 0; i < incNow; i++) {
addNode(i, "head");
}
};
var splitAllNodesA_AAwithState = function () {
// split every node into two nodes
var i = 0;
incNow = incrementer + 1;
//for every node, make a new node
for (i = 0; i < incNow; i++) {
addNode(i, nodes[i].type, nodes[i].state);
}
};
var splitAllNodesB_BAwithState = function () {
// split every node into two nodes
var i = 0;
incNow = incrementer + 1;
//for every node, make a new node
for (i = 0; i < incNow; i++) {
var toChange = nodes[i].type.localeCompare("left") === 0 ||
nodes[i].type.localeCompare("right") === 0 ||
nodes[i].type.localeCompare("normal") === 0;
if (toChange) {
addNode(i, nodes[i].type);
} else {
addNode(i, nodes[i].type, nodes[i].state);
}
}
};
var addNodeIfGen = function () {
if (generating) { addNewNodeToEndOfList(); }
}
// ADDING AND REMOVING NODES }}}
//{{{ HELPER METHODS
function generateAllNeighbours () {
var tmpN = d3.get_delaunay_neighbours(_.map(nodes, function (d) { return [Math.round(d.x), Math.round(d.y)]; }));
var tmpN2 = [];
_.each(tmpN, function (d, i) {
tmpN2.push(_.filter(d, function (x) { return closeNeighbours(nodes[i], nodes[x]); }));
});
return tmpN2;
}
function countNeighbours (index) {
return generateAllNeighbours()[index].length;
}
function checkIfEdge (index) {
//Not a perfect method:
// There are occasional edge nodes which are NOT considered to be edge nodes,
// but it will work fine for us.
// There are also occasional inner nodes which ARE considered to be edge nodes,
// but it's rare enough that it should be okay.
// If we have a cycle that means we are NOT an edge node.
return !findCycle(index)
}
function findCycle (index) {
var neighbours = generateAllNeighbours();
var neighboursOfIndex = neighbours[index];
// there is only one node
if (neighboursOfIndex.length == 0) {
return false;
}
// the neighbours' neighbours, which only include the neighbours.
// AKA we are only considering nodes which are neighbours of index.
var filteredNeighbours = _.map(neighbours, function (nei) { return _.filter(nei, function (nod) {
return neighboursOfIndex.indexOf(nod) > -1;
})});
var previousNode = -1;
var currentNode = neighboursOfIndex[0];
do {
var fil = _.filter(filteredNeighbours[currentNode], function (nod) { return nod != previousNode; });
if (fil.length == 0) return false;
previousNode = currentNode;
currentNode = fil[0];
} while (currentNode != neighboursOfIndex[0])
return true;
//var currentNode = neighboursOfIndex[0];
//var previousNode = -1;
//while (currentNode != neighboursOfIndex[0]) {
//}
//return true;
}
function distanceBetweenTwoPoints(x1, x2, y1, y2) {
return Math.sqrt( Math.pow((x1-x2), 2) + Math.pow((y1-y2), 2));
}
function distanceBetweenTwoNodes(n1, n2) {
return distanceBetweenTwoPoints(n1.x, n2.x, n1.y, n2.y)
}
function closeNeighbours(n1, n2) {
return distanceBetweenTwoNodes(n1, n2) <= cellRadius*2;
}
// HELPER METHODS }}}
// set up the initial mass (mostly for testing)
_.times(1, addNewNodeToEndOfList);
_.times(startingQuantity, splitAllNodesA_AA);
</script>
</body><!--{{{-->
</html><!--}}}-->
<!-- vim: set foldmethod=marker: -->
Modified http://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.4.4/underscore-min.js to a secure url
https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.4.4/underscore-min.js