/** @module World */
let rbush = require('./node_modules/rbush'),
toPath = require('./node_modules/svg-points/').toPath,
saveAs = require('./node_modules/file-saver').saveAs,
Defaults = require('./Defaults');
/** Manages a set of Paths and provides some global control mechanisms, such as pausing the simulation. */
class World {
/**
* Create a new World object
* @param {object} p5 Reference to global p5.js instance
* @param {object} [settings] Object containing local override Settings to be merged with Defaults
* @param {array} [paths] Array of Path objects that belong to this World
*/
constructor(p5, settings = Defaults, paths = []) {
this.p5 = p5;
this.paths = paths;
this.paused = false;
this.settings = Object.assign({}, Defaults, settings);
this.traceMode = this.settings.TraceMode;
this.drawNodes = this.settings.DrawNodes;
this.debugMode = this.settings.DebugMode;
this.invertedColors = this.settings.InvertedColors;
this.fillMode = this.settings.FillMode;
this.drawHistory = this.settings.DrawHistory;
this.useBrownianMotion = this.settings.UseBrownianMotion;
this.showBounds = this.settings.ShowBounds;
this.tree = rbush(9, ['.x','.y','.x','.y']); // use custom accessor strings per https://github.com/mourner/rbush#data-format
this.buildTree();
// Begin capturing path history
let _this = this;
setInterval(function() {
_this.addToHistory();
}, this.settings.HistoryCaptureInterval);
}
/** Run a single "tick" of the simulation by iterating on all Paths */
iterate() {
this.prunePaths();
this.buildTree();
if (this.paths != undefined && this.paths instanceof Array && this.paths.length > 0 && !this.paused) {
for (let path of this.paths) {
path.iterate(this.tree);
}
}
}
/** Draw the background and all Paths */
draw() {
if (!this.traceMode) {
this.drawBackground();
}
for (let path of this.paths) {
path.draw();
}
}
/** Draw the background to the canvas */
drawBackground() {
if(!this.invertedColors) {
this.p5.background(255);
} else {
this.p5.background(0);
}
}
/** Build an R-tree spatial index with all Nodes of all Paths in this World */
buildTree() {
this.tree.clear();
for(let path of this.paths) {
this.tree.load(path.nodes);
}
}
/**
* Add a new Path to the World from outside this class
* @param {object} path Path object to add to this World
*/
addPath(path) {
// Cascade all current World settings to new path
path.drawNodes = this.drawNodes;
path.debugMode = this.debugMode;
path.fillMode = this.fillMode;
path.useBrownianMotion = this.useBrownianMotion;
path.setInvertedColors(this.invertedColors);
path.setTraceMode(this.traceMode);
this.paths.push(path);
}
/**
* Add multiple Path objects to this World
* @param {array} paths
*/
addPaths(paths) {
for(let path of paths) {
this.addPath(path);
}
}
/** Add another snapshot to each Path */
addToHistory() {
if(!this.paused) {
for(let path of this.paths) {
path.addToHistory();
}
}
}
/** Remove any Paths that have gotten too small */
prunePaths() {
for(let i = 0; i < this.paths.length; i++) {
if(this.paths[i].nodes.length <= 1) {
this.paths.splice(i, 1);
}
}
}
/** Generate an SVG file using the current canvas contents and open up a download prompt on the user's machine */
export() {
let svg = document.createElement('svg');
svg.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
svg.setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink');
svg.setAttribute('width', window.innerWidth);
svg.setAttribute('height', window.innerHeight);
svg.setAttribute('viewBox', '0 0 ' + window.innerWidth + ' ' + window.innerHeight);
// Add a <path> node for every Path in this World
for(let path of this.paths) {
// If history is enabled, create a new <path> node for each snapshot
if(this.drawHistory) {
for(let nodes of path.nodeHistory) {
svg.appendChild( this.createPathElFromNodes(nodes, path.isClosed) );
}
}
svg.appendChild( this.createPathElFromNodes(path.nodes), path.isClosed );
}
// Force download of SVG based on https://jsfiddle.net/ch77e7yh/1
let svgDocType = document.implementation.createDocumentType('svg', "-//W3C//DTD SVG 1.1//EN", "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd");
let svgDoc = document.implementation.createDocument('http://www.w3.org/2000/svg', 'svg', svgDocType);
svgDoc.replaceChild(svg, svgDoc.documentElement);
let svgData = (new XMLSerializer()).serializeToString(svgDoc);
let blob = new Blob([svgData.replace(/></g, '>\n\r<')]);
saveAs(blob, 'differential-growth-' + Date.now() + '.svg');
}
/**
* Create a new SVG path element from a provided set of Node objects
* @param {array} nodes Array of Node objects
* @param {boolean} isClosed Whether this path should be closed (true) or open (false)
* @returns SVG path DOM node with a `d` attribute generated from the provided Nodes array.
*/
createPathElFromNodes(nodes, isClosed) {
let pointsString = '';
for(let [index, node] of nodes.entries()) {
pointsString += node.x + ',' + node.y;
if(index < nodes.length - 1) {
pointsString += ' ';
}
}
let d = toPath({
type: 'polyline',
points: pointsString
});
if(isClosed) {
d += ' Z';
}
let pathEl = document.createElement('path');
pathEl.setAttribute('d', d);
pathEl.setAttribute('style', 'fill: none; stroke: black; stroke-width: 1');
return pathEl;
}
/** Remove all Paths from this World */
clearPaths() {
this.paths = [];
}
/** Pause the simulation */
pause() {
this.paused = true;
}
/** Unpause the simulation */
unpause() {
this.paused = false;
}
/**
* Get the current state of the Nodes visibility flag
* @returns {boolean} Current state of Node visibility flag
*/
getDrawNodes() {
return this.drawNodes;
}
/**
* Get the current state of the debug mode flag
* @returns {boolean} Current state of debug mode flag
*/
getDebugMode() {
return this.debugMode;
}
/**
* Get the current state of the fill mode flag
* @returns {boolean} Current state of the fill mode flag
*/
getFillMode() {
return this.fillMode;
}
/**
* Get the current state of the history effect visibility flag
* @returns {boolean} Current state of the history effect visibility flag
*/
getDrawHistory() {
return this.drawHistory;
}
/**
* Get the current state of the Bounds visibility flag
* @returns {boolean} Current state of the Bounds visibility flag
*/
getDrawBounds() {
return this.showBounds;
}
/**
* Set the minimum distance that each Node wants to be from it's connected neighbors
* @param {number} minDistance Distance that each Node wants to be from it's neighbors
*/
setMinDistance(minDistance) {
this.settings.MinDistance = minDistance;
for(let path of this.paths) {
path.setMinDistance(minDistance);
}
}
/**
* Set the maximum distance an edge can be before it is split
* @param {number} maxDistance Distance between each Node
*/
setMaxDistance(maxDistance) {
this.settings.MaxDistance = maxDistance;
for(let path of this.paths) {
path.setMaxDistance(maxDistance);
}
}
/**
* Set the distance around each Node that it can affect other Nodes through repulsion
* @param {number} repulsionRadius Distance around each Node
*/
setRepulsionRadius(repulsionRadius) {
this.settings.RepulsionRadius = repulsionRadius;
for(let path of this.paths) {
path.setRepulsionRadius(repulsionRadius);
}
}
/**
* Set the force scalar that is used when Nodes pull each other closer
* @param {number} attractionForce Scalar value used for attraction force
*/
setAttractionForce(attractionForce) {
this.settings.AttractionForce = attractionForce;
for(let path of this.paths) {
path.setAttractionForce(attractionForce);
}
}
/**
* Set the force scalar that is used when Nodes are pushing others away
* @param {number} repulsionForce Scalar value used for repulsion force
*/
setRepulsionForce(repulsionForce) {
this.settings.RepulsionForce = repulsionForce;
for(let path of this.paths) {
path.setRepulsionForce(repulsionForce);
}
}
/**
* Set the force scalar that is used when Nodes trying to align with their neighbors to reduce curvature
* @param {number} alignmentForce Scalar value used for alignment force
*/
setAlignmentForce(alignmentForce) {
this.settings.AlignmentForce = alignmentForce;
for(let path of this.paths) {
path.setAlignmentForce(alignmentForce);
}
}
/**
* Set the state of the Node visibility flag
* @param {boolean} state Next state for the Node visibility flag
*/
setDrawNodes(state) {
this.drawBackground();
for (let path of this.paths) {
path.drawNodes = state;
path.draw();
}
this.drawNodes = state;
this.settings.DrawNodes = state;
}
/**
* Set the state of the "debug mode" flag
* @param {boolean} state Next state for the "debug mode" flag
*/
setDebugMode(state) {
this.drawBackground();
for (let path of this.paths) {
path.debugMode = state;
path.draw();
}
this.debugMode = state;
this.settings.DebugMode = state;
}
/**
* Set the state of the "fill mode" flag
* @param {boolean} state Next state for the "fill mode" flag
*/
setFillMode(state) {
this.drawBackground();
for(let path of this.paths) {
path.fillMode = state;
path.draw();
}
this.fillMode = state;
this.settings.FillMode = state;
}
/**
* Set the state of the "history" effect flag
* @param {boolean} state Next state for the "history" effect flag
*/
setDrawHistory(state) {
this.drawBackground();
for(let path of this.paths) {
path.drawHistory = state;
path.draw();
}
this.drawHistory = state;
this.settings.DrawHistory = state;
}
/**
* Set the state of the "trace mode" flag
* @param {boolean} state Next state for the "trace mode" flag
*/
setTraceMode(state) {
this.traceMode = state;
this.settings.TraceMode = state;
this.drawBackground();
for(let path of this.paths) {
path.traceMode = state;
}
}
/**
* Set the state of the "invert colors" flag
* @param {boolean} state Next state for the "invert colors" flag
*/
setInvertedColors(state) {
this.invertedColors = state;
this.settings.InvertedColors = state;
this.drawBackground();
for(let path of this.paths) {
path.invertedColors = state;
}
}
/**
* Set the state of the Bounds visibility flag
* @param {boolean} state Next state for the Bounds visibility flag
*/
setDrawBounds(state) {
this.drawBackground();
for(let path of this.paths) {
path.showBounds = state;
path.draw();
}
this.showBounds = state;
}
/** Toggle the state of the Node visibility flag */
toggleDrawNodes() {
this.setDrawNodes(!this.getDrawNodes());
}
/** Toggle the state of the "trace mode" effect flag */
toggleTraceMode() {
this.traceMode = !this.traceMode;
this.drawBackground();
for(let path of this.paths) {
path.toggleTraceMode();
path.draw();
}
}
/** Toggle the state of the "invert colors" flag */
toggleInvertedColors() {
this.invertedColors = !this.invertedColors;
this.drawBackground();
for(let path of this.paths) {
path.toggleInvertedColors();
path.draw();
}
}
/** Toggle the state of the "debug mode" flag */
toggleDebugMode() {
this.setDebugMode(!this.getDebugMode());
}
/** Toggle the state of the "fill mode" flag */
toggleFillMode() {
this.setFillMode(!this.getFillMode());
}
/** Toggle the state of the "history" effect flag */
toggleDrawHistory() {
this.setDrawHistory(!this.getDrawHistory());
}
/** Toggle the state of the Bounds visibility flag */
toggleDrawBounds() {
this.setDrawBounds(!this.getDrawBounds());
}
/** Toggle the pause/unpause state of the simulation */
togglePause() {
if(this.paused) {
this.unpause();
} else {
this.pause();
}
}
}
module.exports = World;