UNPKG

cacatoo

Version:

Building, exploring, and sharing spatially structured models

1,027 lines (907 loc) 90.1 kB
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; }