cacatoo
Version:
Building, exploring, and sharing spatially structured models
1,027 lines (907 loc) • 90.1 kB
JavaScript
import Gridmodel from "./gridmodel"
import Flockmodel from "./flockmodel"
import Canvas from "./canvas"
//import MersenneTwister from '../lib/mersenne'
import * as utility from './utility'
import random from '../lib/random'
/**
* Simulation is the global class of Cacatoo, containing the main configuration
* for making a grid-based model and displaying it in either browser or with
* nodejs.
*/
class Simulation {
/**
* The constructor function for a @Simulation object. Takes a config dictionary.
* and sets options accordingly.
* @param {dictionary} config A dictionary (object) with all the necessary settings to setup a Cacatoo simulation.
*/
constructor(config) {
if(config == undefined) config = {}
this.config = config
this.rng = this.setupRandom(config.seed)
// this.rng_old = new MersenneTwister(config.seed || 53);
this.sleep = config.sleep = config.sleep || 0
this.maxtime = config.maxtime = config.maxtime || 1000000
this.ncol = config.ncol = config.ncol || 100
this.nrow = config.nrow = config.nrow || 100
this.scale = config.scale = config.scale || 2
this.skip = config.skip || 0;
this.graph_interval = config.graph_interval = config.graph_interval || 10
this.graph_update = config.graph_update= config.graph_update || 50
this.fps = config.fps * 1.4 || 60 // Multiplied by 1.4 to adjust for overhead
this.mousecoords = {x:-1000, y:-1000}
// Three arrays for all the grids ('CAs'), canvases ('displays'), and graphs
this.gridmodels = [] // All gridmodels in this simulation
this.flockmodels = [] // All gridmodels in this simulation
this.canvases = [] // Array with refs to all canvases (from all models) from this simulation
this.graphs = [] // All graphs
this.time = 0
this.inbrowser = (typeof document !== "undefined")
this.fpsmeter = false
if(config.fpsmeter == true) this.fpsmeter = true
this.printcursor = true
if(config.printcursor == false) this.printcursor = false
if (this.config.darkmode) this.addDarkModeToggle()
}
/**
* Generate a new GridModel within this simulation.
* @param {string} name The name of your new model, e.g. "gol" for game of life. Cannot contain whitespaces.
*/
makeGridmodel(name) {
if (name.indexOf(' ') >= 0) throw new Error("The name of a gridmodel cannot contain whitespaces.")
let model = new Gridmodel(name, this.config, this.rng) // ,this.config.show_gridname weggecomment
this[name] = model // this = model["cheater"] = CA-obj
this.gridmodels.push(model)
}
/**
* Generate a new GridModel within this simulation.
* @param {string} name The name of your new model, e.g. "gol" for game of life. Cannot contain whitespaces.
*/
makeFlockmodel(name, cfg) {
let cfg_combined = {...this.config,...cfg}
if (name.indexOf(' ') >= 0) throw new Error("The name of a gridmodel cannot contain whitespaces.")
let model = new Flockmodel(name, cfg_combined, this.rng) // ,this.config.show_gridname weggecomment
this[name] = model // this = model["cheater"] = CA-obj
this.flockmodels.push(model)
}
/**
* Set up the random number generator
* @param {int} seed Seed for fast-random module
*/
setupRandom(seed){
// Load mersennetwister random number generator
// genrand_real1() [0,1]
// genrand_real2() [0,1)
// genrand_real3() (0,1)
// genrand_int(min,max) integer between min and max
// let rng = new MersenneTwister(seed || 53) // Use this if you need a more precise RNG
let rng = random(seed);
rng.genrand_real1 = function () { return (rng.nextInt() - 1) / 2147483645 } // Generate random number in [0,1] range
rng.genrand_real2 = function () { return (rng.nextInt() - 1) / 2147483646 } // Generate random number in [0,1) range
rng.genrand_real3 = function () { return rng.nextInt() / 2147483647 } // Generate random number in (0,1) range
rng.genrand_int = function (min,max) { return min+ rng.nextInt() % (max-min+1) } // Generate random integer between (and including) min and max
for(let i = 0; i < 1000; i++) rng.genrand_real2()
rng.random = () => { return rng.genrand_real2() }
rng.randomInt = () => { return rng.genrand_int() }
rng.randomGaus = (mean=0, stdev=1) => { // Standard gaussian sample
const u = 1 - sim.rng.random();
const v = sim.rng.random();
const z = Math.sqrt( -2.0 * Math.log( u ) ) * Math.cos( 2.0 * Math.PI * v );
return z * stdev + mean;
}
return rng
}
/**
* Create a display for a gridmodel, showing a certain property on it.
* @param {string} name The name of an existing gridmodel to display
* @param {string} property The name of the property to display
* @param {string} customlab Overwrite the display name with something more descriptive
* @param {integer} height Number of rows to display (default = ALL)
* @param {integer} width Number of cols to display (default = ALL)
* @param {integer} scale Scale of display (default inherited from @Simulation class)
*/
createDisplay(name, property, customlab, height, width, scale) {
if(! this.inbrowser) {
console.warn("Cacatoo:createDisplay, cannot create display in command-line mode.")
return
}
if(typeof arguments[0] === 'object')
{
name = arguments[0].name
property = arguments[0].property
customlab = arguments[0].label
height = arguments[0].height
width = arguments[0].width
scale = arguments[0].scale
}
if(name==undefined || property == undefined) throw new Error("Cacatoo: can't make a display with out a 'name' and 'property'")
let label = customlab
if (customlab == undefined) label = `${name} (${property})` // <ID>_NAME_(PROPERTY)
let gridmodel = this[name]
if (gridmodel == undefined) throw new Error(`There is no GridModel with the name ${name}`)
if (height == undefined) height = gridmodel.nr
if (width == undefined) width = gridmodel.nc
if (scale == undefined) scale = gridmodel.scale
if(gridmodel.statecolours[property]==undefined){
console.log(`Cacatoo: no fill colour supplied for property ${property}. Using default and hoping for the best.`)
gridmodel.statecolours[property] = utility.default_colours(10)
}
let cnv = new Canvas(gridmodel, property, label, height, width, scale);
gridmodel.canvases[label] = cnv // Add a reference to the canvas to the gridmodel
this.canvases.push(cnv) // Add a reference to the canvas to the sim
const canvas = cnv
cnv.add_legend(cnv.canvasdiv,property)
cnv.bgcolour = this.config.bgcolour || 'black'
canvas.elem.addEventListener('mousedown', (e) => { this.printCursorPosition(canvas, e, scale) }, false)
canvas.elem.addEventListener('mousedown', (e) => { this.active_canvas = canvas }, false)
cnv.displaygrid()
}
createGridDisplay = this.createDisplay
/**
* Create a display for a flockmodel, showing the boids
* @param {string} name The name of an existing gridmodel to display
* @param {Object} config All the options for the flocking behaviour etc.
*/
createFlockDisplay(name, config = {}) {
if(! this.inbrowser) {
console.warn("Cacatoo:createFlockDisplay, cannot create display in command-line mode.")
return
}
if(name==undefined) throw new Error("Cacatoo: can't make a display with out a 'name'")
let flockmodel = this[name]
if (flockmodel == undefined) throw new Error(`There is no GridModel with the name ${name}`)
let property = config.property
let addToDisplay = config.addToDisplay
let label = config.label || ""
let legendlabel = config.legendlabel
let height = sim.height || flockmodel.height
let width = config.width || sim.width || flockmodel.width
let scale = config.scale || this[name].scale
let maxval = config.maxval || this.maxval || undefined
let decimals= config.decimals || 0
let nticks= config.nticks || 5
let minval = config.minval || 0
let num_colours = config.num_colours || 100
if(config.fill == "viridis") this[name].colourViridis(property, num_colours)
else if(config.fill == "inferno") this[name].colourViridis(property, num_colours, false, "inferno")
else if(config.fill == "rainbow") this[name].statecolours[property] = this[name].colourGradientArray(num_colours, 0,[251, 169, 73], [250, 228, 66], [139, 212, 72], [42, 168, 242],[50,100,255])
else if(config.fill == "pride") this[name].statecolours[property] = this[name].colourGradientArray(num_colours, 0, [228, 3, 3], [255, 140, 0], [255, 237, 0], [0, 128, 38], [0,76,255],[115,41,130])
else if(config.fill == "inferno_rev") this[name].colourViridis(property, num_colours, true, "inferno")
else if(config.fill == "red") this[name].colourGradient(property, num_colours, [0, 0, 0], [255, 0, 0])
else if(config.fill == "green") this[name].colourGradient(property, num_colours, [0, 0, 0], [0, 255, 0])
else if(config.fill == "blue") this[name].colourGradient(property, num_colours, [0, 0, 0], [0, 0, 255])
else if(this[name].statecolours[property]==undefined && property){
console.log(`Cacatoo: no fill colour supplied for property ${property}. Using default and hoping for the best.`)
this[name].colourGradient(property, num_colours, [0, 0, 0], [0, 0, 255])
}
let cnv = new Canvas(flockmodel, property, label, height, width,scale,false,addToDisplay);
if (maxval !== undefined) cnv.maxval = maxval
if (minval !== undefined) cnv.minval = minval
if (num_colours !== undefined) cnv.num_colours = num_colours
if (decimals !== undefined) cnv.decimals = decimals
if (nticks !== undefined) cnv.nticks = nticks
cnv.strokeStyle = config.strokeStyle
cnv.strokeWidth = config.strokeWidth
if(config.legend!==false){
if(addToDisplay) cnv.add_legend(addToDisplay.canvasdiv,property,legendlabel)
else cnv.add_legend(cnv.canvasdiv,property,legendlabel )
}
// Set the shape of the boid
let shape = flockmodel.config.shape || 'dot'
cnv.drawBoid = cnv.drawBoidArrow
if(shape == 'bird') cnv.drawBoid = cnv.drawBoidBird
else if(shape == 'arrow') cnv.drawBoid = cnv.drawBoidArrow
else if(shape == 'rect') cnv.drawBoid = cnv.drawBoidRect
else if(shape == 'dot') cnv.drawBoid = cnv.drawBoidPoint
else if(shape == 'ant') cnv.drawBoid = cnv.drawBoidAnt
else if(shape == 'bear') cnv.drawBoid = cnv.drawBoidBear
else if(shape == 'bunny') cnv.drawBoid = cnv.drawBoidBunny
else if(shape == 'rod') cnv.drawBoid = cnv.drawBoidRod
else if(shape == 'line') cnv.drawBoid = cnv.drawBoidLine
else if(shape == 'png') cnv.drawBoid = cnv.drawBoidPng
// Replace function that handles the left mousebutton
let click = flockmodel.config.click || 'none'
if(click == 'repel') flockmodel.handleMouseBoids = flockmodel.repelBoids
else if(click == 'pull') flockmodel.handleMouseBoids = flockmodel.pullBoids
else if(click == 'kill') flockmodel.handleMouseBoids = flockmodel.killBoids
flockmodel.canvases[label] = cnv // Add a reference to the canvas to the flockmodel
this.canvases.push(cnv) // Add a reference to the canvas to the sim
const canvas = addToDisplay || cnv
flockmodel.mouseDown = false
flockmodel.mouseClick = false
canvas.elem.addEventListener('mousemove', (e) => {
let mouse = this.getCursorPosition(canvas,e,1,false)
if(mouse.x == this.mousecoords.x && mouse.y == this.mousecoords.y) return this.mousecoords
this.mousecoords = {x:mouse.x/this.scale, y:mouse.y/this.scale}
flockmodel.mousecoords = this.mousecoords
})
canvas.elem.addEventListener('touchmove', (e) => {
let mouse = this.getCursorPosition(canvas,e.touches[0],1,false)
if(mouse.x == this.mousecoords.x && mouse.y == this.mousecoords.y) return this.mousecoords
this.mousecoords = {x:mouse.x/this.scale, y:mouse.y/this.scale}
flockmodel.mousecoords = this.mousecoords
e.preventDefault();
})
canvas.elem.addEventListener('mousedown', (e) => { flockmodel.mouseDown = true})
canvas.elem.addEventListener('touchstart', (e) => { flockmodel.mouseDown = true})
canvas.elem.addEventListener('mouseup', (e) => { flockmodel.mouseDown = false })
canvas.elem.addEventListener('touchend', (e) => { flockmodel.mouseDown = false; flockmodel.mousecoords = {x:-1000,y:-1000}})
canvas.elem.addEventListener('mouseout', (e) => { flockmodel.mousecoords = {x:-1000,y:-1000}})
cnv.bgcolour = this.config.bgcolour || 'black'
cnv.display = cnv.displayflock
cnv.display()
}
/**
* Create a display for a gridmodel, showing a certain property on it.
* @param {object} config Object with the keys name, property, label, width, height, scale, minval, maxval, nticks, decimals, num_colours, fill
* These keys:value pairs are:
* @param {string} name The name of the model to display
* @param {string} property The name of the property to display
* @param {string} customlab Overwrite the display name with something more descriptive
* @param {integer} height Number of rows to display (default = ALL)
* @param {integer} width Number of cols to display (default = ALL)
* @param {integer} scale Scale of display (default inherited from @Simulation class)
* @param {numeric} minval colour scale is capped off below this value
* @param {numeric} maxval colour scale is capped off above this value
* @param {integer} nticks how many ticks
* @param {integer} decimals how many decimals for tick labels
* @param {integer} num_colours how many steps in the colour gradient
* @param {string} fill type of gradient to use (viridis, inferno, red, green, blue)
*/
createDisplay_discrete(config) {
if(! this.inbrowser) {
console.warn("Cacatoo:createDisplay_discrete, cannot create display in command-line mode.")
return
}
let name = config.model
let property = config.property
let legend = true
if(config.legend == false) legend = false
let label = config.label
if (label == undefined) label = `${name} (${property})` // <ID>_NAME_(PROPERTY)
let gridmodel = this[name]
if (gridmodel == undefined) throw new Error(`There is no GridModel with the name ${name}`)
let height = config.height || this[name].nr
let width = config.width || this[name].nc
let scale = config.scale || this[name].scale
let legendlabel = config.legendlabel
if(name==undefined || property == undefined) throw new Error("Cacatoo: can't make a display with out a 'name' and 'property'")
if (gridmodel == undefined) throw new Error(`There is no GridModel with the name ${name}`)
if (height == undefined) height = gridmodel.nr
if (width == undefined) width = gridmodel.nc
if (scale == undefined) scale = gridmodel.scale
if(gridmodel.statecolours[property]==undefined){
console.log(`Cacatoo: no fill colour supplied for property ${property}. Using default and hoping for the best.`)
gridmodel.statecolours[property] = utility.default_colours(10)
}
let cnv = new Canvas(gridmodel, property, label, height, width, scale);
if(config.drawdots) {
cnv.display = cnv.displaygrid_dots
cnv.stroke = config.stroke
cnv.strokeStyle = config.strokeStyle
cnv.strokeWidth = config.strokeWidth
cnv.radius = config.radius || 10
cnv.max_radius = config.max_radius || 10
cnv.scale_radius = config.scale_radius || 1
cnv.min_radius = config.min_radius || 0
}
gridmodel.canvases[label] = cnv // Add a reference to the canvas to the gridmodel
this.canvases.push(cnv) // Add a reference to the canvas to the sim
const canvas = cnv
if(legend) cnv.add_legend(cnv.canvasdiv,property, legendlabel)
cnv.bgcolour = this.config.bgcolour || 'black'
canvas.elem.addEventListener('mousedown', (e) => { this.printCursorPosition(canvas, e, scale) }, false)
canvas.elem.addEventListener('mousedown', (e) => { this.active_canvas = canvas }, false)
cnv.displaygrid()
}
/**
* Create a display for a gridmodel, showing a certain property on it.
* @param {object} config Object with the keys name, property, label, width, height, scale, minval, maxval, nticks, decimals, num_colours, fill
* These keys:value pairs are:
* @param {string} name The name of the model to display
* @param {string} property The name of the property to display
* @param {string} customlab Overwrite the display name with something more descriptive
* @param {integer} height Number of rows to display (default = ALL)
* @param {integer} width Number of cols to display (default = ALL)
* @param {integer} scale Scale of display (default inherited from @Simulation class)
* @param {numeric} minval colour scale is capped off below this value
* @param {numeric} maxval colour scale is capped off above this value
* @param {integer} nticks how many ticks
* @param {integer} decimals how many decimals for tick labels
* @param {integer} num_colours how many steps in the colour gradient
* @param {string} fill type of gradient to use (viridis, inferno, red, green, blue)
*/
createDisplay_continuous(config) {
if(! this.inbrowser) {
console.warn("Cacatoo:createDisplay_continuous, cannot create display in command-line mode.")
return
}
let name = config.model
let property = config.property
let label = config.label
let legendlabel = config.legendlabel
let legend = true
if(config.legend == false) legend = false
if (label == undefined) label = `${name} (${property})` // <ID>_NAME_(PROPERTY)
let gridmodel = this[name]
if (gridmodel == undefined) throw new Error(`There is no GridModel with the name ${name}`)
let height = config.height || this[name].nr
let width = config.width || this[name].nc
let scale = config.scale || this[name].scale
let maxval = config.maxval || this.maxval || undefined
let decimals= config.decimals || 0
let nticks= config.nticks || 5
let minval = config.minval || 0
let num_colours = config.num_colours || 100
if(config.fill == "viridis") this[name].colourViridis(property, num_colours)
else if(config.fill == "inferno") this[name].colourViridis(property, num_colours, false, "inferno")
else if(config.fill == "rainbow") this[name].statecolours[property] = this[name].colourGradientArray(num_colours, 0,[251, 169, 73], [250, 228, 66], [139, 212, 72], [42, 168, 242],[50,100,255])
else if(config.fill == "pride") this[name].statecolours[property] = this[name].colourGradientArray(num_colours, 0, [228, 3, 3], [255, 140, 0], [255, 237, 0], [0, 128, 38], [0,76,255],[115,41,130])
else if(config.fill == "inferno_rev") this[name].colourViridis(property, num_colours, true, "inferno")
else if(config.fill == "red") this[name].colourGradient(property, num_colours, [0, 0, 0], [255, 0, 0])
else if(config.fill == "green") this[name].colourGradient(property, num_colours, [0, 0, 0], [0, 255, 0])
else if(config.fill == "blue") this[name].colourGradient(property, num_colours, [0, 0, 0], [0, 0, 255])
else if(this[name].statecolours[property]==undefined){
console.log(`Cacatoo: no fill colour supplied for property ${property}. Using default and hoping for the best.`)
this[name].colourGradient(property, num_colours, [0, 0, 0], [0, 0, 255])
}
let cnv = new Canvas(gridmodel, property, label, height, width, scale, true);
if(config.drawdots) {
cnv.display = cnv.displaygrid_dots
cnv.stroke = config.stroke
cnv.strokeStyle = config.strokeStyle
cnv.strokeWidth = config.strokeWidth
cnv.radius = config.radius || 10
cnv.max_radius = config.max_radius || 10
cnv.scale_radius = config.scale_radius || 1
cnv.min_radius = config.min_radius || 0
}
gridmodel.canvases[label] = cnv // Add a reference to the canvas to the gridmodel
if (maxval !== undefined) cnv.maxval = maxval
if (minval !== undefined) cnv.minval = minval
if (num_colours !== undefined) cnv.num_colours = num_colours
if (decimals !== undefined) cnv.decimals = decimals
if (nticks !== undefined) cnv.nticks = nticks
if(legend!==false) cnv.add_legend(cnv.canvasdiv,property,legendlabel)
cnv.bgcolour = this.config.bgcolour || 'black'
this.canvases.push(cnv) // Add a reference to the canvas to the sim
const canvas = cnv
canvas.elem.addEventListener('mousedown', (e) => { this.printCursorPosition(cnv, e, scale) }, false)
canvas.elem.addEventListener('mousedown', (e) => { this.active_canvas = canvas }, false)
cnv.displaygrid()
}
/**
* Create a space time display for a gridmodel
* @param {string} name The name of an existing gridmodel to display
* @param {string} source_canvas_label The name of the property to display
* @param {string} label Overwrite the display name with something more descriptive
* @param {integer} col_to_draw Col to display (default = center)
* @param {integer} ncol Number of cols (i.e. time points) to display (default = ncol)
* @param {integer} scale Scale of display (default inherited from @Simulation class)
*/
spaceTimePlot(name, source_canvas_label, label, col_to_draw, ncolumn) {
if(! this.inbrowser) {
console.warn("Cacatoo:spaceTimePlot, cannot create display in command-line mode.")
return
}
let source_canvas = this[name].canvases[source_canvas_label]
let property = source_canvas.property
let height = source_canvas.height
let width = ncolumn
let scale = source_canvas.scale
let cnv = new Canvas(this[name], property, label, height, width, scale);
cnv.spacetime=true
cnv.offset_x = col_to_draw
cnv.continuous = source_canvas.continuous
cnv.minval = source_canvas.minval
cnv.maxval = source_canvas.maxval
cnv.num_colours = source_canvas.num_colours
cnv.ctx.fillRect(0, 0, width*scale , height*scale)
this[name].canvases[label] = cnv // Add a reference to the canvas to the gridmodel
this.canvases.push(cnv) // Add a reference to the canvas to the sim
const canvas = cnv
cnv.radius = source_canvas.radius
cnv.bgcolour = source_canvas.bgcolour
cnv.add_legend(cnv.canvasdiv, property, "")
cnv.bgcolour = this.config.bgcolour || 'black'
cnv.elem.addEventListener('mousedown', (e) => { sim.active_canvas = cnv }, false)
}
/**
* Get the position of the cursor on the canvas
* @param {canvas} canvas A (constant) canvas object
* @param {event-handler} event Event handler (mousedown)
* @param {scale} scale The zoom (scale) of the grid to grab the correct grid point
*/
getCursorPosition(canvas, event, scale, floor=true) {
const rect = canvas.elem.getBoundingClientRect()
let x = Math.max(0, event.clientX - rect.left) / scale + canvas.offset_x
let y = Math.max(0, event.clientY - rect.top) / scale + canvas.offset_y
if(floor){
x = Math.floor(x)
y = Math.floor(y)
}
return({x:x,y:y})
}
/**
* Get *and print the GP* at the cursor position
* @param {canvas} canvas A (constant) canvas object
* @param {event-handler} event Event handler (mousedown)
* @param {scale} scale The zoom (scale) of the grid to grab the correct grid point
*/
printCursorPosition(canvas, event, scale){
if(!this.printcursor) return
let coords = this.getCursorPosition(canvas,event,scale)
let x = coords.x
let y = coords.y
if( x< 0 || x >= this.ncol || y < 0 || y >= this.nrow) return
console.log(`You have clicked the grid at position ${x},${y}, which has:`)
for (let model of this.gridmodels) {
console.log("grid point:", model.grid[x][y])
}
for (let model of this.flockmodels) {
console.log("boids:", model.mouseboids)
}
}
/**
* Update all the grid models one step. Apply optional mixing
*/
step() {
for (let i = 0; i < this.gridmodels.length; i++){
this.gridmodels[i].update()
this.gridmodels[i].time++
}
for (let i = 0; i < this.flockmodels.length; i++){
let model = this.flockmodels[i]
model.flock()
model.update()
model.time++
let mouse = model.mousecoords
if(model.mouse_radius) model.mouseboids = model.getIndividualsInRange(mouse,model.mouse_radius)
if(model.mouseDown)model.handleMouseBoids()
}
for (let i = 0; i < this.canvases.length; i++)
if(this.canvases[i].recording == true)
this.captureFrame(this.canvases[i])
this.time++
}
/**
* Apply global events to all grids in the model.
* (only perfectmix currently... :D)
*/
events() {
for (let i = 0; i < this.gridmodels.length; i++) {
if (this.mix) this.gridmodels[i].perfectMix()
}
}
/**
* Display all the canvases linked to this simulation
*/
display() {
for (let i = 0; i < this.canvases.length; i++){
this.canvases[i].display()
if(this.canvases[i].recording == true){
this.captureFrame(this.canvases[i])
}
}
}
/**
* Start the simulation. start() detects whether the user is running the code from the browser or, alternatively,
* in nodejs. In the browser, a GUI is provided to interact with the model. In nodejs the
* programmer can simply wait for the result without wasting time on displaying intermediate stuff
* (which can be slow)
*/
start() {
let sim = this; // Caching this, as function animate changes the this-scope to the scope of the animate-function
let meter = undefined;
if (this.inbrowser) {
if(this.fpsmeter){
meter = new FPSMeter({ position: 'absolute', width: '30px', show: 'fps', left: "auto", top: "65px", right: "25px", graph: 1, history: 20, smoothing: 100});
}
if (this.config.noheader != true) {
document.title = `Cacatoo - ${this.config.title}`;
var link = document.querySelector("link[rel~='icon']");
if (!link) { link = document.createElement('link'); link.rel = 'icon'; document.getElementsByTagName('head')[0].appendChild(link); }
link.href = '../../images/favicon.png';
}
if (document.getElementById("footer") != null) document.getElementById("footer").innerHTML = `<a target="blank" href="https://bramvandijk88.github.io/cacatoo/"><img class="logos" src=""https://bramvandijk88.github.io/cacatoo/images/elephant_cacatoo_small.png"></a>`;
if (document.getElementById("footer") != null) document.getElementById("footer").innerHTML += `<a target="blank" href="https://github.com/bramvandijk88/cacatoo"><img class="logos" style="padding-top:32px;" src=""https://bramvandijk88.github.io/cacatoo/images/gh.png"></a></img>`;
if (this.config.noheader != true && document.getElementById("header") != null) document.getElementById("header").innerHTML = `<div style="height:40px;"><h2>Cacatoo - ${this.config.title}</h2></div><div style="padding-bottom:20px;"><font size=2>${this.config.description}</font size></div>`;
if (document.getElementById("footer") != null) document.getElementById("footer").innerHTML += "<h2><u><a href=\"https://bramvandijk88.github.io/cacatoo\" target=\"_blank\">Cacatoo</a></u> is a toolbox to explore spatially structured models straight from your webbrowser. Suggestions or issues can be reported <u><a href=\"https://github.com/bramvandijk88/cacatoo/issues\" target=\"_blank\">here</a></u>.</h2>";
let simStartTime = performance.now();
async function animate() {
if (sim.sleep > 0) await pause(sim.sleep);
if(sim.fpsmeter) meter.tickStart();
if (!sim.pause == true) {
sim.step();
sim.events();
}
if(sim.time%(sim.skip+1)==0) sim.display();
if(sim.fpsmeter) meter.tick();
let frame = requestAnimationFrame(animate);
if (sim.time > sim.config.maxtime || sim.exit) {
let simStopTime = performance.now();
console.log("Cacatoo completed after", Math.round(simStopTime - simStartTime) / 1000, "seconds");
cancelAnimationFrame(frame);
}
}
requestAnimationFrame(animate);
}
else {
while (true) {
sim.step();
if (sim.time > sim.config.maxtime || sim.exit ) {
console.log("Cacatoo completed.");
return true;
}
}
}
}
/**
* Stop the simulation
*/
stop() {
this.exit = true
}
/**
* initialGrid populates a grid with states. The number of arguments
* is flexible and defined the percentage of every state. For example,
* initialGrid('grid','species',1,0.5,2,0.25) populates the grid with
* two species (1 and 2), where species 1 occupies 50% of the grid, and
* species 2 25%.
* @param {@GridModel} grid The gridmodel containing the grid to be modified.
* @param {String} property The name of the state to be set
* @param {integer} value The value of the state to be set (optional argument with position 2, 4, 6, ..., n)
* @param {float} fraction The chance the grid point is set to this state (optional argument with position 3, 5, 7, ..., n)
*/
initialGrid(obj,property,defaultvalue=0) {
// First, make sure you have access to the appropriate gridmodel
let gridmodel = undefined
if (obj instanceof Gridmodel) // user passed a gridmodel object
gridmodel = obj
else
gridmodel = obj.gridmodel
if(typeof gridmodel === 'string' || gridmodel instanceof String) // user passed a string
gridmodel = this[gridmodel]
// Next, put the default values in the grid
let p = property || obj.property || 'val'
if(obj.default != undefined) defaultvalue = obj.default
for (let x = 0; x < gridmodel.nc; x++) // x are columns
for (let y = 0; y < gridmodel.nr; y++) // y are rows
gridmodel.grid[x][y][p] = defaultvalue
let frequencies = obj.frequencies || []
for (let arg = 3; arg < arguments.length; arg += 2) {
let value = arguments[arg];
let fract = arguments[arg + 1];
frequencies.push([value,fract]);
}
for (let x = 0; x < gridmodel.nc; x++) // x are columns
for (let y = 0; y < gridmodel.nr; y++){ // y are rows
let rand = this.rng.random();
let fract = 0
for(let k of frequencies){
fract += k[1];
if(rand < fract){
gridmodel.grid[x][y][p] = k[0];
break;
}
}
}
}
/**
* populateGrid populates a grid with custom individuals.
* @param {@GridModel} grid The gridmodel containing the grid to be modified.
* @param {Array} individuals The properties for individuals 1..n
* @param {Array} freqs The initial frequency of individuals 1..n
*/
populateGrid(gridmodel,individuals,freqs)
{
if(typeof gridmodel === 'string' || gridmodel instanceof String) gridmodel = this[gridmodel]
if(individuals.length != freqs.length) throw new Error("populateGrid should have as many individuals as frequencies")
if(freqs.reduce((a, b) => a + b) > 1) throw new Error("populateGrid should not have frequencies that sum up to greater than 1")
for (let x = 0; x < gridmodel.nc; x++) // x are columns
for (let y = 0; y < gridmodel.nr; y++){ // y are rows
for (const property in individuals[0]) {
gridmodel.grid[x][y][property] = 0;
}
let random_number = this.rng.random()
let sum_freqs = 0
for(let n=0; n<individuals.length; n++)
{
sum_freqs += freqs[n]
if(random_number < sum_freqs) {
gridmodel.grid[x][y] = {...gridmodel.grid[x][y],...JSON.parse(JSON.stringify(individuals[n]))};
break
}
}
}
}
/**
* initialSpot populates a grid with states. Grid points close to a certain coordinate are set to state value, while
* other cells are set to the bg-state of 0.
* @param {@GridModel} grid The gridmodel containing the grid to be modified.
* @param {String} property The name of the state to be set
* @param {integer} value The value of the state to be set (optional argument with position 2, 4, 6, ..., n)
*/
initialSpot(gridmodel, property, value, size, x, y,background_state=false) {
if(typeof gridmodel === 'string' || gridmodel instanceof String) gridmodel = this[gridmodel]
let p = property || 'val'
for (let x = 0; x < gridmodel.nc; x++) // x are columns
for (let y = 0; y < gridmodel.nr; y++)
if(background_state) gridmodel.grid[x % gridmodel.nc][y % gridmodel.nr][p] = background_state
this.putSpot(gridmodel,property,value,size,x,y)
}
/**
* putSpot sets values at a certain position with a certain radius. Grid points close to a certain coordinate are set to state value, while
* other cells are set to the bg-state of 0.
* @param {@GridModel} grid The gridmodel containing the grid to be modified.
* @param {String} property The name of the state to be set
* @param {integer} value The value of the state to be set (optional argument with position 2, 4, 6, ..., n)
* @param {float} fraction The chance the grid point is set to this state (optional argument with position 3, 5, 7, ..., n)
*/
putSpot(gridmodel, property, value, size, putx, puty) {
if(typeof gridmodel === 'string' || gridmodel instanceof String) gridmodel = this[gridmodel]
// Draw a circle
for (let x = 0; x < gridmodel.nc; x++) // x are columns
for (let y = 0; y < gridmodel.nr; y++) // y are rows
{
if ((Math.pow((x - putx), 2) + Math.pow((y - puty), 2)) < size)
gridmodel.grid[x % gridmodel.nc][y % gridmodel.nr][property] = value
}
}
/**
* populateSpot populates a spot with custom individuals.
* @param {@GridModel} grid The gridmodel containing the grid to be modified.
* @param {Array} individuals The properties for individuals 1..n
* @param {Array} freqs The initial frequency of individuals 1..n
*/
populateSpot(gridmodel,individuals, freqs,size, putx, puty, background_state=false)
{
if(typeof gridmodel === 'string' || gridmodel instanceof String) gridmodel = this[gridmodel]
let sumfreqs =0
if(individuals.length != freqs.length) throw new Error("populateGrid should have as many individuals as frequencies")
for(let i=0; i<freqs.length; i++) sumfreqs += freqs[i]
// Draw a circle
for (let x = 0; x < gridmodel.nc; x++) // x are columns
for (let y = 0; y < gridmodel.nr; y++) // y are rows
{
if ((Math.pow((x - putx), 2) + Math.pow((y - puty), 2)) < size)
{
let cumsumfreq = 0
let rand = this.rng.random()
for(let n=0; n<individuals.length; n++)
{
cumsumfreq += freqs[n]
if(rand < cumsumfreq) {
Object.assign(gridmodel.grid[x % gridmodel.nc][y % gridmodel.nr],individuals[n])
break
}
}
}
else if(background_state) Object.assign(gridmodel.grid[x][y], background_state)
}
}
/**
* addButton adds a HTML button which can be linked to a function by the user.
* @param {string} text Text displayed on the button
* @param {function} func Function to be linked to the button
*/
addButton(text, func) {
if (!this.inbrowser) return
let button = document.createElement("button")
button.innerHTML = text;
button.id = text
button.addEventListener("click", func, true);
document.getElementById("form_holder").appendChild(button)
}
/**
* addButton adds a HTML button, in this case a default "pause button"
* @param {boolean} start Whether the simulation should start in a paused state
*/
addPauseButton(start=true) {
if (!this.inbrowser) return
let button = document.createElement("button")
button.innerHTML = "▶ / ⏸";
button.id = "pausebutton"
button.style.width = "70px"
//button.style.height = "34px"
button.style.fontSize = "11px"
button.addEventListener("click", () => this.toggle_play(), true);
document.getElementById("form_holder").appendChild(button)
if(!start) this.toggle_play()
}
/**
* addSlider adds a HTML slider to the DOM-environment which allows the user
* to modify a model parameter at runtime.
* @param {string} parameter The name of the (global!) parameter to link to the slider
* @param {float} [min] Minimal value of the slider
* @param {float} [max] Maximum value of the slider
* @param {float} [step] Step-size when modifying
*/
addSlider(parameter, min = 0.0, max = 2.0, step = 0.01, label) {
let lab = label || parameter
if (!this.inbrowser) return
if (window[parameter] === undefined) { console.warn(`addSlider: parameter ${parameter} not found. No slider made.`); return; }
let container = document.createElement("div")
container.classList.add("form-container")
let slider = document.createElement("input")
let numeric = document.createElement("input")
container.innerHTML += "<div style='width:100%;height:20px;font-size:12px;'><b>" + lab + ":</b></div>"
// Setting slider variables / handler
slider.type = 'range'
slider.classList.add("slider")
slider.min = min
slider.max = max
slider.step = step
slider.value = window[parameter]
slider.oninput = function () {
let value = parseFloat(slider.value)
window[parameter] = parseFloat(value)
numeric.value = value
}
// Setting number variables / handler
numeric.type = 'number'
numeric.classList.add("number")
numeric.min = min
numeric.max = max
numeric.step = step
numeric.value = window[parameter]
numeric.onchange = function () {
let value = parseFloat(numeric.value)
if (value > this.max) value = this.max
if (value < this.min) value = this.min
window[parameter] = parseFloat(value)
numeric.value = value
slider.value = value
}
container.appendChild(slider)
container.appendChild(numeric)
document.getElementById("form_holder").appendChild(container)
}
/**
* addCustomSlider adds a HTML slider to the DOM-environment which allows the user
* to add a custom callback function to a slider
* @param {function} func The name of the (global!) parameter to link to the slider
* @param {float} [min] Minimal value of the slider
* @param {float} [max] Maximum value of the slider
* @param {float} [step] Step-size when modifying
*/
addCustomSlider(label,func, min = 0.0, max = 2.0, step = 0.01, default_value=0) {
let lab = label || func
if (!this.inbrowser) return
if (func === undefined) { console.warn(`addCustomSlider: callback function not defined. No slider made.`); return; }
let container = document.createElement("div")
container.classList.add("form-container")
let slider = document.createElement("input")
let numeric = document.createElement("input")
container.innerHTML += "<div style='width:100%;height:20px;font-size:12px;'><b>" + lab + ":</b></div>"
// Setting slider variables / handler
slider.type = 'range'
slider.classList.add("slider")
slider.min = min
slider.max = max
slider.step = step
slider.value = default_value
//let parent = sim
slider.oninput = function () {
let value = parseFloat(slider.value)
func(value)
numeric.value = value
}
// Setting number variables / handler
numeric.type = 'number'
numeric.classList.add("number")
numeric.min = min
numeric.max = max
numeric.step = step
numeric.value = default_value
numeric.onchange = function () {
let value = parseFloat(numeric.value)
if (value > this.max) value = this.max
if (value < this.min) value = this.min
func(value)
numeric.value = value
slider.value = value
}
container.appendChild(slider)
container.appendChild(numeric)
document.getElementById("form_holder").appendChild(container)
}
/**
* save a PNG of an entire HTML div element
* @param {div} div object to store to
*/
async sectionToPNG(div, prefix) {
if (!this.inbrowser) {
console.warn("[Cacatoo] sectionToPNG is browser-only.")
return
}
const elem = typeof div === 'string' ? document.getElementById(div) : div
if (!elem) { console.warn(`[Cacatoo] sectionToPNG: element not found`); return }
const timestamp = String(sim.time + 1).padStart(6, '0')
const filename = `${prefix}${timestamp}.png`
const canvas = await html2canvas(elem)
if (this.outputDirHandle) {
const blob = await new Promise(res => canvas.toBlob(res, 'image/png'))
const fh = await this.outputDirHandle.getFileHandle(filename, { create: true })
const ws = await fh.createWritable()
await ws.write(blob)
await ws.close()
} else {
// fallback: download (original behaviour)
const link = document.createElement('a')
link.download = filename
link.href = canvas.toDataURL()
link.click()
}
}
/**
* recordVideo captures the canvas to an webm-video (browser only)
* @param {canvas} canvas Canvas object to record
*/
startRecording(canvas,fps){
if(!canvas.recording){
canvas.recording = true
canvas.elem.style.outline = '4px solid red';
sim.capturer = new CCapture( { format: 'webm',
quality: 100,
name: `${canvas.label}_starttime_${sim.time}`,
framerate: fps,
display: false } );
sim.capturer.start()
console.log("Started recording video.")
}
}
captureFrame(canvas){
if(canvas.recording){
sim.capturer.capture(canvas.elem)
}
}
stopRecording(canvas){
if(canvas.recording){
canvas.recording = false
canvas.elem.style.outline = '0px';
sim.capturer.stop()
sim.capturer.save();
console.log("Video saved")
}
}
makeMovie(canvas, fps=60){
if(this.sleep > 0) throw new Error("Cacatoo not combine makeMovie with sleep. Instead, set sleep to 0 and set the framerate of the movie: makeMovie(canvas, fps).")
if(!sim.recording){
sim.startRecording(canvas,fps)
sim.recording=true
}
else{
sim.stopRecording(canvas)
sim.recording=false
}
}
async pickOutputDir(path = null) {
if (!this.inbrowser) {
const fs = require('fs')
if (!path) throw new Error("In Node mode, provide a path: sim.pickOutputDir('./output')")
if (!fs.existsSync(path)) fs.mkdirSync(path, { recursive: true })
this.outputDir = path
console.log(`[Cacatoo] Output directory set to: ${path}`)
} else {
if (!('showDirectoryPicker' in window)) {
console.warn("[Cacatoo] File System Access API not supported. Use Chrome or Edge.")
return
}
try {
this.outputDirHandle = await window.showDirectoryPicker({ mode: 'readwrite' })
console.log(`[Cacatoo] Output directory: ${this.outputDirHandle.name}`)
} catch (e) {
console.log("[Cacatoo] Directory picker cancelled.")
}
}
}
/**
* addToggle adds a HTML checkbox element to the DOM-environment which allows the user
* to flip boolean values
* @param {string} parameter The name of the (global!) boolean to link to the checkbox
*/
addToggle(parameter, label, func) {
let lab = label || parameter
if (!this.inbrowser) return
if (window[parameter] === undefined) { console.warn(`addToggle: parameter ${parameter} not found. No toggle made.`); return; }