Source: cacatoo.js

  1. 'use strict';
  2. /**
  3. * Gridpoint is what Gridmodels are made of. Contains everything that may happen in 1 locality.
  4. */
  5. class Gridpoint {
  6. /**
  7. * The constructor function for a @Gridpoint object. Takes an optional template to copy primitives from. (NOTE!! Other types of objects are NOT deep copied by default)
  8. * If you need synchronous updating with complex objects (for whatever reason), replate line 18 with line 19. This will slow things down quite a bit, so ony use this
  9. * if you really need it. A better option is to use asynchronous updating so you won't have to worry about this at all :)
  10. * @param {Gridpoint} template Optional template to make a new @Gridpoint from
  11. */
  12. constructor(template) {
  13. for (var prop in template)
  14. this[prop] = template[prop]; // Shallow copy. It's fast, but be careful with syncronous updating!
  15. }
  16. }
  17. /**
  18. * Graph is a wrapper-class for a Dygraph element (see https://dygraphs.com/). It is attached to the DOM-windows, and stores all values to be plotted, colours, title, axis names, etc.
  19. */
  20. class Graph {
  21. /**
  22. * The constructor function for a @Canvas object.
  23. * @param {Array} labels array of strings containing the labels for datapoints (e.g. for the legend)
  24. * @param {Array} values Array of floats to plot (here plotted over time)
  25. * @param {Array} colours Array of colours to use for plotting
  26. * @param {String} title Title of the plot
  27. * @param {Object} opts dictionary-style list of opts to pass onto dygraphs
  28. */
  29. constructor(labels, values, colours, title, opts) {
  30. if (typeof window == undefined) throw "Using dygraphs with cashJS only works in browser-mode"
  31. this.labels = labels;
  32. this.data = [values];
  33. this.title = title;
  34. this.num_dps = values.length; // number of data points for this graphs
  35. this.elem = document.createElement("div");
  36. this.elem.className = "graph-holder";
  37. this.colours = [];
  38. for (let v of colours) {
  39. if (v == "Time") continue
  40. else if (v == undefined) this.colours.push("#000000");
  41. else this.colours.push(rgbToHex$1(v[0], v[1], v[2]));
  42. }
  43. document.body.appendChild(this.elem);
  44. document.getElementById("graph_holder").appendChild(this.elem);
  45. let graph_opts = {title: this.title,
  46. showRoller: false,
  47. width: opts ? (opts.width != undefined ? opts.width : 500) : 500,
  48. labelsSeparateLines: true,
  49. height: opts ? (opts.height != undefined ? opts.height : 200) : 200,
  50. xlabel: this.labels[0],
  51. ylabel: this.labels.length == 2 ? this.labels[1] : "",
  52. drawPoints: opts ? (opts.drawPoints ? opts.drawPoints : false) : false,
  53. pointSize: opts ? (opts.pointSize ? opts.pointSize : 0) : 0,
  54. logscale: opts ? (opts.logscale ? opts.logscale : false) : false,
  55. strokePattern: opts ? (opts.strokePattern != undefined ? opts.strokePattern : null) : null,
  56. dateWindow: [0, 100],
  57. axisLabelFontSize: 10,
  58. valueRange: [opts ? (opts.min_y != undefined ? opts.min_y: 0):0, opts ? (opts.max_y != undefined ? opts.max_y: null):null],
  59. strokeWidth: opts ? (opts.strokeWidth != undefined ? opts.strokeWidth : 3) : 3,
  60. colors: this.colours,
  61. labels: labels.length == values.length ? this.labels: null,
  62. series: opts ? ( opts.series != undefined ? opts.series : null) : null
  63. };
  64. for(var opt in opts){
  65. graph_opts[opt] = opts[opt];
  66. }
  67. this.g = new Dygraph(this.elem, this.data, graph_opts);
  68. }
  69. /** Push data to your graph-element
  70. * @param {array} array of floats to be added to the dygraph object (stored in 'data')
  71. */
  72. push_data(data_array) {
  73. this.data.push(data_array);
  74. }
  75. reset_plot() {
  76. let first_dp = this.data[0];
  77. this.data = [];
  78. let empty = Array(first_dp.length).fill(undefined);
  79. this.data.push(empty);
  80. this.g.updateOptions(
  81. {
  82. 'file': this.data
  83. });
  84. }
  85. /**
  86. * Update the graph axes
  87. */
  88. update() {
  89. let max_x = 0;
  90. let min_x = 999999999999;
  91. for (let i of this.data) {
  92. if (i[0] > max_x) max_x = i[0];
  93. if (i[0] < min_x) min_x = i[0];
  94. }
  95. this.g.updateOptions(
  96. {
  97. 'file': this.data,
  98. dateWindow: [min_x, max_x]
  99. });
  100. }
  101. }
  102. /*
  103. Functions below are to make sure dygraphs understands the colours used by Cacatoo (converts to hex)
  104. */
  105. function componentToHex$1(c) {
  106. var hex = c.toString(16);
  107. return hex.length == 1 ? "0" + hex : hex;
  108. }
  109. function rgbToHex$1(r, g, b) {
  110. return "#" + componentToHex$1(r) + componentToHex$1(g) + componentToHex$1(b);
  111. }
  112. /**
  113. * The ODE class is used to call the odex.js library and numerically solve ODEs
  114. */
  115. class ODE {
  116. /**
  117. * The constructor function for a @ODE object.
  118. * @param {function} eq Function that describes the ODE (see examples starting with ode)
  119. * @param {Array} state_vector Initial state vector
  120. * @param {Array} pars Array of parameters for the ODEs
  121. * @param {Array} diff_rates Array of rates at which each state diffuses to neighbouring grid point (Has to be less than 0.25!)
  122. * @param {String} ode_name Name of this ODE
  123. */
  124. constructor(eq, state_vector, pars, diff_rates, ode_name, acceptable_error) {
  125. this.name = ode_name;
  126. this.eq = eq;
  127. this.state = state_vector;
  128. this.diff_rates = diff_rates;
  129. this.pars = pars;
  130. this.solver = new Solver(state_vector.length);
  131. if (acceptable_error !== undefined) this.solver.absoluteTolerance = this.solver.relativeTolerance = acceptable_error;
  132. }
  133. /**
  134. * Numerically solve the ODE
  135. * @param {float} delta_t Step size
  136. * @param {bool} opt_pos When enabled, negative values are set to 0 automatically
  137. */
  138. solveTimestep(delta_t = 0.1, pos = false) {
  139. let newstate = this.solver.solve(
  140. this.eq(...this.pars), // function to solve and its pars (... unlists the array as a list of args)
  141. 0, // Initial x value
  142. this.state, // Initial y value(s)
  143. delta_t // Final x value
  144. ).y;
  145. if (pos) for (var i = 0; i < newstate.length; i++) if (newstate[i] < 0.000001) newstate[i] = 0.0;
  146. this.state = newstate;
  147. }
  148. /**
  149. * Prints the current state to the console
  150. */
  151. print_state() {
  152. console.log(this.state);
  153. }
  154. }
  155. /**
  156. * Reverse dictionary
  157. * @param {Object} obj dictionary-style object to reverse in order
  158. */
  159. function dict_reverse(obj) {
  160. let new_obj = {};
  161. let rev_obj = Object.keys(obj).reverse();
  162. rev_obj.forEach(function (i) {
  163. new_obj[i] = obj[i];
  164. });
  165. return new_obj;
  166. }
  167. /**
  168. * Randomly shuffle an array with custom RNG
  169. * @param {Array} array array to be shuffled
  170. * @param {MersenneTwister} rng MersenneTwister RNG
  171. */
  172. function shuffle(array, rng) {
  173. let i = array.length;
  174. while (i--) {
  175. const ri = Math.floor(rng.random() * (i + 1));
  176. [array[i], array[ri]] = [array[ri], array[i]];
  177. }
  178. return array;
  179. }
  180. /**
  181. * Convert colour string to RGB. Works for colour names ('red','blue' or other colours defined in cacatoo), but also for hexadecimal strings
  182. * @param {String} string string to convert to RGB
  183. */
  184. function stringToRGB(string) {
  185. if (string[0] != '#') return nameToRGB(string)
  186. else return hexToRGB(string)
  187. }
  188. /**
  189. * Convert hexadecimal to RGB
  190. * @param {String} hex string to convert to RGB
  191. */
  192. function hexToRGB(hex) {
  193. var result;
  194. if(hex.length == 7) {
  195. result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
  196. return [parseInt(result[1], 16), parseInt(result[2], 16), parseInt(result[3], 16)]
  197. }
  198. if(hex.length == 9) {
  199. result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
  200. return [parseInt(result[1], 16), parseInt(result[2], 16), parseInt(result[3], 16), parseInt(result[4], 16)]
  201. }
  202. }
  203. /**
  204. * Convert colour name to RGB
  205. * @param {String} name string to look up in the set of known colours (see below)
  206. */
  207. function nameToRGB(string) {
  208. let colours = {
  209. 'black': [0, 0, 0],
  210. 'white': [255, 255, 255],
  211. 'red': [255, 0, 0],
  212. 'blue': [0, 0, 255],
  213. 'green': [0, 255, 0],
  214. 'darkgrey': [40, 40, 40],
  215. 'lightgrey': [180, 180, 180],
  216. 'violet': [148, 0, 211],
  217. 'turquoise': [64, 224, 208],
  218. 'orange': [255, 165, 0],
  219. 'gold': [240, 200, 0],
  220. 'grey': [125, 125, 125],
  221. 'yellow': [255, 255, 0],
  222. 'cyan': [0, 255, 255],
  223. 'aqua': [0, 255, 255],
  224. 'silver': [192, 192, 192],
  225. 'nearwhite': [192, 192, 192],
  226. 'purple': [128, 0, 128],
  227. 'darkgreen': [0, 128, 0],
  228. 'olive': [128, 128, 0],
  229. 'teal': [0, 128, 128],
  230. 'navy': [0, 0, 128]
  231. };
  232. let c = colours[string];
  233. if (c == undefined) throw new Error(`Cacatoo has no colour with name '${string}'`)
  234. return c
  235. }
  236. /**
  237. * Make sure all colours, even when of different types, are stored in the same format (RGB, as cacatoo uses internally)
  238. * @param {Array} cols array of strings, or [R,G,B]-arrays. Only strings are converted, other returned.
  239. */
  240. function parseColours(cols) {
  241. let return_cols = [];
  242. for (let c of cols) {
  243. if (typeof c === 'string' || c instanceof String) {
  244. return_cols.push(stringToRGB(c));
  245. }
  246. else {
  247. return_cols.push(c);
  248. }
  249. }
  250. return return_cols
  251. }
  252. /**
  253. * Compile a dict of default colours if nothing is given by the user. Reuses colours if more colours are needed.
  254. */
  255. function default_colours(num_colours)
  256. {
  257. let colour_dict = [
  258. [0, 0, 0], // black
  259. [255, 255, 255], // white
  260. [255, 0, 0], // red
  261. [0, 0, 255], // blue
  262. [0, 255, 0], //green
  263. [60, 60, 60], //darkgrey
  264. [180, 180, 180], //lightgrey
  265. [148, 0, 211], //violet
  266. [64, 224, 208], //turquoise
  267. [255, 165, 0], //orange
  268. [240, 200, 0], //gold
  269. [125, 125, 125],
  270. [255, 255, 0], // yellow
  271. [0, 255, 255], // cyan
  272. [192, 192, 192], // silver
  273. [0, 128, 0], //darkgreen
  274. [128, 128, 0], // olive
  275. [0, 128, 128], // teal
  276. [0, 0, 128]]; // navy
  277. let return_dict = {};
  278. for(let i = 0; i < num_colours; i++)
  279. {
  280. return_dict[i] = colour_dict[i%19];
  281. }
  282. return return_dict
  283. }
  284. /**
  285. * A list of default colours if nothing is given by the user.
  286. */
  287. function random_colours(num_colours,rng)
  288. {
  289. let return_dict = {};
  290. return_dict[0] = [0,0,0];
  291. for(let i = 1; i < num_colours; i++)
  292. {
  293. return_dict[i] = [rng.genrand_int(0,255),rng.genrand_int(0,255),rng.genrand_int(0,255)];
  294. }
  295. return return_dict
  296. }
  297. /**
  298. * Deep copy function.
  299. * @param {Object} aObject Object to be deep copied. This function still won't deep copy every possible object, so when enabling deep copying, make sure you put your debug-hat on!
  300. */
  301. function copy(aObject) {
  302. if (!aObject) {
  303. return aObject;
  304. }
  305. let v;
  306. let bObject = Array.isArray(aObject) ? [] : {};
  307. for (const k in aObject) {
  308. v = aObject[k];
  309. bObject[k] = (typeof v === "object") ? copy(v) : v;
  310. }
  311. return bObject;
  312. }
  313. /**
  314. * Gridmodel is the main type of model in Cacatoo. Most of these models
  315. * will look and feel like CAs, but GridModels can also contain ODEs with diffusion, making
  316. * them more like PDEs.
  317. */
  318. class Gridmodel {
  319. /**
  320. * The constructor function for a @Gridmodel object. Takes the same config dictionary as used in @Simulation
  321. * @param {string} name The name of your model. This is how it will be listed in @Simulation 's properties
  322. * @param {dictionary} config A dictionary (object) with all the necessary settings to setup a Cacatoo GridModel.
  323. * @param {MersenneTwister} rng A random number generator (MersenneTwister object)
  324. */
  325. constructor(name, config={}, rng) {
  326. this.name = name;
  327. this.time = 0;
  328. this.nc = config.ncol || 200;
  329. this.nr = config.nrow || 200;
  330. this.grid = MakeGrid(this.nc, this.nr); // Initialises an (empty) grid
  331. this.grid_buffer = MakeGrid(this.nc, this.nr); // Initialises an (empty) grid
  332. this.wrap = config.wrap || [true, true];
  333. this.rng = rng;
  334. this.random = () => { return this.rng.random()};
  335. this.randomInt = (a,b) => { return this.rng.randomInt(a,b)};
  336. this.statecolours = this.setupColours(config.statecolours,config.num_colours); // Makes sure the statecolours in the config dict are parsed (see below)
  337. this.scale = config.scale || 1;
  338. this.graph_update = config.graph_update || 20;
  339. this.graph_interval = config.graph_interval || 2;
  340. this.bgcolour = config.bgcolour || 'black';
  341. this.margolus_phase = 0;
  342. // Store a simple array to get neighbours from the N, E, S, W, NW, NE, SW, SE (analogous to Cash2.1)
  343. this.moore = [[0, 0], // SELF _____________
  344. [0, -1], // NORTH | 5 | 1 | 6 |
  345. [-1, 0], // WEST | 2 | 0 | 3 |
  346. [1, 0], // EAST | 7 | 4 | 8 |
  347. [0, 1], // SOUTH _____________
  348. [-1, -1], // NW
  349. [1, -1], // NE
  350. [-1, 1], // SW
  351. [1, 1] // SE
  352. ];
  353. this.graphs = {}; // Object containing all graphs belonging to this model (HTML usage only)
  354. this.canvases = {}; // Object containing all Canvases belonging to this model (HTML usage only)
  355. }
  356. /** Replaces current grid with an empty grid */
  357. clearGrid()
  358. {
  359. this.grid = MakeGrid(this.nc,this.nr);
  360. }
  361. /**
  362. * Saves the current grid in a JSON object. In browser mode, it will throw download-request, which may or may not
  363. * work depending on the security of the user's browser.
  364. * @param {string} filename The name of of the JSON file
  365. */
  366. save_grid(filename)
  367. {
  368. console.log(`Saving grid in JSON file \'${filename}\'`);
  369. let gridjson = JSON.stringify(this.grid);
  370. if((typeof document !== "undefined")){
  371. const a = document.createElement('a');
  372. a.href = URL.createObjectURL( new Blob([gridjson], { type:'text/plain' }) );
  373. a.download = filename;
  374. a.click();
  375. console.warn("Cacatoo: download of grid in browser-mode may be blocked for security reasons.");
  376. return
  377. }
  378. else {
  379. try { var fs = require('fs'); }
  380. catch (e) {
  381. console.log('Cacatoo:save_grid: save_grid requires file-system module. Please install fs via \'npm install fs\'');
  382. }
  383. fs.writeFileSync(filename, gridjson, function(err) {
  384. if (err) {
  385. console.log(err);
  386. }
  387. });
  388. }
  389. }
  390. /**
  391. * Reads a JSON file and loads a JSON object onto this gridmodel. Reading a local JSON file will not work in browser mode because of security reasons,
  392. * You can instead use 'addCheckpointButton' instead, which allows you to select a file from the browser manually.
  393. * @param {string} file Path to the json file
  394. */
  395. load_grid(file)
  396. {
  397. if((typeof document !== "undefined")){
  398. console.warn("Cacatoo: loading grids directly is not supported in browser-mode for security reasons. Use 'addCheckpointButton' instead. ");
  399. return
  400. }
  401. this.clearGrid();
  402. console.log(`Loading grid for ${this.name} from file \'${file}\'`);
  403. try { var fs = require('fs'); }
  404. catch (e) {
  405. console.log('Cacatoo:load_grid: requires file-system module. Please install fs via \'npm install fs\'');
  406. }
  407. let filehandler = fs.readFileSync(file);
  408. let gridjson = JSON.parse(filehandler);
  409. this.grid_from_json(gridjson);
  410. }
  411. /**
  412. * Loads a JSON object onto this gridmodel.
  413. * @param {string} gridjson JSON object to build new grid from
  414. */
  415. grid_from_json(gridjson)
  416. {
  417. for(let x in gridjson)
  418. for(let y in gridjson[x])
  419. {
  420. let newgp = new Gridpoint(gridjson[x][y]);
  421. gridjson[x][y] = newgp;
  422. }
  423. this.grid = gridjson;
  424. }
  425. /** Print the entire grid to the console */
  426. print_grid() {
  427. console.table(this.grid);
  428. }
  429. /** Initiate a dictionary with colour arrays [R,G,B] used by Graph and Canvas classes
  430. * @param {statecols} object - given object can be in two forms
  431. * | either {state:colour} tuple (e.g. 'alive':'white', see gol.html)
  432. * | or {state:object} where objects are {val:'colour},
  433. * | e.g. {'species':{0:"black", 1:"#DDDDDD", 2:"red"}}, see cheater.html
  434. */
  435. setupColours(statecols,num_colours=18) {
  436. let return_dict = {};
  437. if (statecols == null) // If the user did not define statecols (yet)
  438. return return_dict["state"] = default_colours(num_colours)
  439. let colours = dict_reverse(statecols) || { 'val': 1 };
  440. for (const [statekey, statedict] of Object.entries(colours)) {
  441. if (statedict == 'default') {
  442. return_dict[statekey] = default_colours(num_colours+1);
  443. }
  444. else if (statedict == 'random') {
  445. return_dict[statekey] = random_colours(num_colours+1,this.rng);
  446. }
  447. else if (statedict == 'viridis') {
  448. let colours = this.colourGradientArray(num_colours, 0,[68, 1, 84], [59, 82, 139], [33, 144, 140], [93, 201, 99], [253, 231, 37]);
  449. return_dict[statekey] = colours;
  450. }
  451. else if (statedict == 'inferno') {
  452. let colours = this.colourGradientArray(num_colours, 0,[20, 11, 52], [132, 32, 107], [229, 92, 45], [246, 215, 70]);
  453. return_dict[statekey] = colours;
  454. }
  455. else if (statedict == 'inferno_rev') {
  456. let colours = this.colourGradientArray(num_colours, 0, [246, 215, 70], [229, 92, 45], [132, 32, 107]);
  457. return_dict[statekey] = colours;
  458. }
  459. else if (typeof statedict === 'string' || statedict instanceof String) // For if
  460. {
  461. return_dict[statekey] = stringToRGB(statedict);
  462. }
  463. else {
  464. let c = {};
  465. for (const [key, val] of Object.entries(statedict)) {
  466. if (Array.isArray(val)) c[key] = val;
  467. else c[key] = stringToRGB(val);
  468. }
  469. return_dict[statekey] = c;
  470. }
  471. }
  472. return return_dict
  473. }
  474. /** Initiate a gradient of colours for a property (return array only)
  475. * @param {string} property The name of the property to which the colour is assigned
  476. * @param {int} n How many colours the gradient consists off
  477. * For example usage, see colourViridis below
  478. */
  479. colourGradientArray(n,total)
  480. {
  481. let color_dict = {};
  482. //color_dict[0] = [0, 0, 0]
  483. let n_arrays = arguments.length - 2;
  484. if (n_arrays <= 1) throw new Error("colourGradient needs at least 2 arrays")
  485. let segment_len = Math.ceil(n / (n_arrays-1));
  486. if(n <= 10 && n_arrays > 3) console.warn("Cacatoo warning: forming a complex gradient with only few colours... hoping for the best.");
  487. let total_added_colours = 0;
  488. for (let arr = 0; arr < n_arrays - 1 ; arr++) {
  489. let arr1 = arguments[2 + arr];
  490. let arr2 = arguments[2 + arr + 1];
  491. for (let i = 0; i < segment_len; i++) {
  492. let r, g, b;
  493. if (arr2[0] > arr1[0]) r = Math.floor(arr1[0] + (arr2[0] - arr1[0])*( i / (segment_len-1) ));
  494. else r = Math.floor(arr1[0] - (arr1[0] - arr2[0]) * (i / (segment_len-1)));
  495. if (arr2[1] > arr1[1]) g = Math.floor(arr1[1] + (arr2[1] - arr1[1]) * (i / (segment_len - 1)));
  496. else g = Math.floor(arr1[1] - (arr1[1] - arr2[1]) * (i / (segment_len - 1)));
  497. if (arr2[2] > arr1[2]) b = Math.floor(arr1[2] + (arr2[2] - arr1[2]) * (i / (segment_len - 1)));
  498. else b = Math.floor(arr1[2] - (arr1[2] - arr2[2]) * (i / (segment_len - 1)));
  499. color_dict[Math.floor(i + arr * segment_len + total)] = [Math.min(r,255), Math.min(g,255), Math.min(b,255)];
  500. total_added_colours++;
  501. if(total_added_colours == n) break
  502. }
  503. color_dict[n] = arguments[arguments.length-1];
  504. }
  505. return(color_dict)
  506. }
  507. /** Initiate a gradient of colours for a property.
  508. * @param {string} property The name of the property to which the colour is assigned
  509. * @param {int} n How many colours the gradient consists off
  510. * For example usage, see colourViridis below
  511. */
  512. colourGradient(property, n) {
  513. let offset = 2;
  514. let n_arrays = arguments.length - offset;
  515. if (n_arrays <= 1) throw new Error("colourGradient needs at least 2 arrays")
  516. let color_dict = {};
  517. let total = 0;
  518. if(this.statecolours !== undefined && this.statecolours[property] !== undefined){
  519. color_dict = this.statecolours[property];
  520. total = Object.keys(this.statecolours[property]).length;
  521. }
  522. let all_arrays = [];
  523. for (let arr = 0; arr < n_arrays ; arr++) all_arrays.push(arguments[offset + arr]);
  524. let new_dict = this.colourGradientArray(n,total,...all_arrays);
  525. this.statecolours[property] = {...color_dict,...new_dict};
  526. }
  527. /** Initiate a gradient of colours for a property, using the Viridis colour scheme (purpleblue-ish to green to yellow) or Inferno (black to orange to yellow)
  528. * @param {string} property The name of the property to which the colour is assigned
  529. * @param {int} n How many colours the gradient consists off
  530. * @param {bool} rev Reverse the viridis colour gradient
  531. */
  532. colourViridis(property, n, rev = false, option="viridis") {
  533. if(option=="viridis"){
  534. if (!rev) this.colourGradient(property, n, [68, 1, 84], [59, 82, 139], [33, 144, 140], [93, 201, 99], [253, 231, 37]); // Viridis
  535. else this.colourGradient(property, n, [253, 231, 37], [93, 201, 99], [33, 144, 140], [59, 82, 139], [68, 1, 84]); // Viridis
  536. }
  537. else if(option=="inferno"){
  538. if (!rev) this.colourGradient(property, n, [20, 11, 52], [132, 32, 107], [229, 92, 45], [246, 215, 70]); // Inferno
  539. else this.colourGradient(property, n, [246, 215, 70], [229, 92, 45], [132, 32, 107], [20, 11, 52]); // Inferno
  540. }
  541. }
  542. /** The most important function in GridModel: how to determine the next state of a gridpoint?
  543. * By default, nextState is empty. It should be defined by the user (see examples)
  544. * @param {int} x Position of grid point to update (column)
  545. * @param {int} y Position of grid point to update (row)
  546. */
  547. nextState(x, y) {
  548. throw 'Nextstate function of \'' + this.name + '\' undefined';
  549. }
  550. /** Synchronously apply the nextState function (defined by user) to the entire grid
  551. * Synchronous means that all grid points will be updated simultaneously. This is ensured
  552. * by making a back-up grid, which will serve as a reference to know the state in the previous
  553. * time step. First all grid points are updated based on the back-up. Only then will the
  554. * actual grid be changed.
  555. */
  556. synchronous() {
  557. let oldstate = MakeGrid(this.nc, this.nr, this.grid); // Create a copy of the current grid
  558. let newstate = MakeGrid(this.nc, this.nr); // Create an empty grid for the next state
  559. for (let x = 0; x < this.nc; x++) {
  560. for (let y = 0; y < this.nr; y++) {
  561. this.nextState(x, y); // Update this.grid[x][y]
  562. newstate[x][y] = this.grid[x][y]; // Store new state in newstate
  563. this.grid[x][y] = oldstate[x][y]; // Restore original state
  564. }
  565. }
  566. this.grid = newstate; // Replace the current grid with the newly computed one
  567. }
  568. /** Like the synchronous function above, but can not take a custom user-defined function rather
  569. * than the default next-state function. Technically one should be able to refarctor this by making
  570. * the default function of synchronous "nextstate". But this works. :)
  571. */
  572. apply_sync(func) {
  573. let oldstate = MakeGrid(this.nc, this.nr, this.grid); // Old state based on current grid
  574. let newstate = MakeGrid(this.nc, this.nr); // New state == empty grid
  575. for (let x = 0; x < this.nc; x++) {
  576. for (let y = 0; y < this.nr; y++) {
  577. func(x, y); // Update this.grid
  578. newstate[x][y] = this.grid[x][y]; // Set this.grid to newstate
  579. this.grid[x][y] = oldstate[x][y]; // Reset this.grid to old state
  580. }
  581. }
  582. this.grid = newstate;
  583. }
  584. /** Asynchronously apply the nextState function (defined by user) to the entire grid
  585. * Asynchronous means that all grid points will be updated in a random order. For this
  586. * first the update_order will be determined (this.set_update_order). Afterwards, the nextState
  587. * will be applied in that order. This means that some cells may update while all their neighours
  588. * are still un-updated, and other cells will update while all their neighbours are already done.
  589. */
  590. asynchronous() {
  591. this.set_update_order();
  592. for (let n = 0; n < this.nc * this.nr; n++) {
  593. let m = this.upd_order[n];
  594. let x = m % this.nc;
  595. let y = Math.floor(m / this.nc);
  596. this.nextState(x, y);
  597. }
  598. // Don't have to copy the grid here. Just cycle through x,y in random order and apply nextState :)
  599. }
  600. /** Analogous to apply_sync(func), but asynchronous */
  601. apply_async(func) {
  602. this.set_update_order();
  603. for (let n = 0; n < this.nc * this.nr; n++) {
  604. let m = this.upd_order[n];
  605. let x = m % this.nc;
  606. let y = Math.floor(m / this.nc);
  607. func(x, y);
  608. }
  609. }
  610. /** If called for the first time, make an update order (list of ints), otherwise just shuffle it. */
  611. set_update_order() {
  612. if (typeof this.upd_order === 'undefined') // "Static" variable, only create this array once and reuse it
  613. {
  614. this.upd_order = [];
  615. for (let n = 0; n < this.nc * this.nr; n++) {
  616. this.upd_order.push(n);
  617. }
  618. }
  619. shuffle(this.upd_order, this.rng); // Shuffle the update order
  620. }
  621. /** The update is, like nextState, user-defined (hence, empty by default).
  622. * It should contains all functions that one wants to apply every time step
  623. * (e.g. grid manipulations and printing statistics)
  624. * For example, and update function could look like:
  625. * this.synchronous() // Update all cells
  626. * this.MargolusDiffusion() // Apply Toffoli Margolus diffusion algorithm
  627. * this.plotPopsizes('species',[1,2,3]) // Plot the population sizes
  628. */
  629. update() {
  630. throw 'Update function of \'' + this.name + '\' undefined';
  631. }
  632. /** Get the gridpoint at coordinates x,y
  633. * Makes sure wrapping is applied if necessary
  634. * @param {int} xpos position (column) for the focal gridpoint
  635. * @param {int} ypos position (row) for the focal gridpoint
  636. */
  637. getGridpoint(xpos, ypos) {
  638. let x = xpos;
  639. if (this.wrap[0]) x = (xpos + this.nc) % this.nc; // Wraps neighbours left-to-right
  640. let y = ypos;
  641. if (this.wrap[1]) y = (ypos + this.nr) % this.nr; // Wraps neighbours top-to-bottom
  642. if (x < 0 || y < 0 || x >= this.nc || y >= this.nr) return undefined // If sampling neighbour outside of the grid, return empty object
  643. else return this.grid[x][y]
  644. }
  645. /** Change the gridpoint at position x,y into gp (typically retrieved with 'getGridpoint')
  646. * Makes sure wrapping is applied if necessary
  647. * @param {int} x position (column) for the focal gridpoint
  648. * @param {int} y position (row) for the focal gridpoint
  649. * @param {Gridpoint} @Gridpoint object to set the gp to (result of 'getGridpoint')
  650. */
  651. setGridpoint(xpos, ypos, gp) {
  652. let x = xpos;
  653. if (this.wrap[0]) x = (xpos + this.nc) % this.nc; // Wraps neighbours left-to-right
  654. let y = ypos;
  655. if (this.wrap[1]) y = (ypos + this.nr) % this.nr; // Wraps neighbours top-to-bottom
  656. if (x < 0 || y < 0 || x >= this.nc || y >= this.nr) this.grid[x][y] = undefined;
  657. else this.grid[x][y] = gp;
  658. }
  659. /** Return a copy of the gridpoint at position x,y
  660. * Makes sure wrapping is applied if necessary
  661. * @param {int} x position (column) for the focal gridpoint
  662. * @param {int} y position (row) for the focal gridpoint
  663. */
  664. copyGridpoint(xpos, ypos) {
  665. let x = xpos;
  666. if (this.wrap[0]) x = (xpos + this.nc) % this.nc; // Wraps neighbours left-to-right
  667. let y = ypos;
  668. if (this.wrap[1]) y = (ypos + this.nr) % this.nr; // Wraps neighbours top-to-bottom
  669. if (x < 0 || y < 0 || x >= this.nc || y >= this.nr) return undefined
  670. else {
  671. return new Gridpoint(this.grid[x][y])
  672. }
  673. }
  674. /** Change the gridpoint at position x,y into gp
  675. * Makes sure wrapping is applied if necessary
  676. * @param {int} x position (column) for the focal gridpoint
  677. * @param {int} y position (row) for the focal gridpoint
  678. * @param {Gridpoint} @Gridpoint object to set the gp to
  679. */
  680. copyIntoGridpoint(xpos, ypos, gp) {
  681. let x = xpos;
  682. if (this.wrap[0]) x = (xpos + this.nc) % this.nc; // Wraps neighbours left-to-right
  683. let y = ypos;
  684. if (this.wrap[1]) y = (ypos + this.nr) % this.nr; // Wraps neighbours top-to-bottom
  685. if (x < 0 || y < 0 || x >= this.nc || y >= this.nr) this.grid[x][y] = undefined;
  686. else {
  687. for (var prop in gp)
  688. this.grid[x][y][prop] = gp[prop];
  689. }
  690. }
  691. /** Get the x,y coordinates of a neighbour in an array.
  692. * Makes sure wrapping is applied if necessary
  693. */
  694. getNeighXY(xpos, ypos) {
  695. let x = xpos;
  696. if (this.wrap[0]) x = (xpos + this.nc) % this.nc; // Wraps neighbours left-to-right
  697. let y = ypos;
  698. if (this.wrap[1]) y = (ypos + this.nr) % this.nr; // Wraps neighbours top-to-bottom
  699. if (x < 0 || y < 0 || x >= this.nc || y >= this.nr) return undefined // If sampling neighbour outside of the grid, return empty object
  700. else return [x, y]
  701. }
  702. /** Get a neighbour at compass direction
  703. * @param {GridModel} grid The gridmodel used to check neighbours. Usually the gridmodel itself (i.e., this),
  704. * but can be mixed to make grids interact.
  705. * @param {int} col position (column) for the focal gridpoint
  706. * @param {int} row position (row) for the focal gridpoint
  707. * @param {int} direction the neighbour to return
  708. */
  709. getNeighbour(model,col,row,direction) {
  710. let x = model.moore[direction][0];
  711. let y = model.moore[direction][1];
  712. return model.getGridpoint(col + x, row + y)
  713. }
  714. /** Get array of grid points with val in property (Neu4, Neu5, Moore8, Moore9 depending on range-array)
  715. * @param {GridModel} grid The gridmodel used to check neighbours. Usually the gridmodel itself (i.e., this),
  716. * but can be mixed to make grids interact.
  717. * @param {int} col position (column) for the focal gridpoint
  718. * @param {int} row position (row) for the focal gridpoint
  719. * @param {string} property the property that is counted
  720. * @param {int} val value 'property' should have
  721. * @param {Array} range which section of the neighbourhood must be counted? (see this.moore, e.g. 1-8 is Moore8, 0-4 is Neu5,etc)
  722. * @return {int} The number of grid points with "property" set to "val"
  723. * Below, 4 version of this functions are overloaded (Moore8, Moore9, Neumann4, etc.)
  724. * If one wants to count all the "cheater" surrounding a gridpoint in cheater.js in the Moore8 neighbourhood
  725. * one needs to look for value '3' in the property 'species':
  726. * this.getNeighbours(this,10,10,3,'species',[1-8]);
  727. * or
  728. * this.getMoore8(this,10,10,3,'species')
  729. */
  730. getNeighbours(model,col,row,property,val,range) {
  731. let gps = [];
  732. for (let n = range[0]; n <= range[1]; n++) {
  733. let x = model.moore[n][0];
  734. let y = model.moore[n][1];
  735. let neigh = model.getGridpoint(col + x, row + y);
  736. if (neigh != undefined && neigh[property] == val)
  737. gps.push(neigh);
  738. }
  739. return gps;
  740. }
  741. /** getNeighbours for the Moore8 neighbourhood (range 1-8 in function getNeighbours) */
  742. getMoore8(model, col, row, property,val) { return this.getNeighbours(model,col,row,property,val,[1,8]) }
  743. getNeighbours8(model, col, row, property,val) { return this.getNeighbours(model,col,row,property,val,[1,8]) }
  744. /** getNeighbours for the Moore8 neighbourhood (range 1-8 in function getNeighbours) */
  745. getMoore9(model, col, row, property,val) { return this.getNeighbours(model,col,row,property,val,[0,8]) }
  746. getNeighbours9(model, col, row, property,val) { return this.getNeighbours(model,col,row,property,val,[0,8]) }
  747. /** getNeighbours for the Moore8 neighbourhood (range 1-8 in function getNeighbours) */
  748. getNeumann4(model, col, row, property,val) { return this.getNeighbours(model,col,row,property,val,[1,4]) }
  749. getNeighbours4(model, col, row, property,val) { return this.getNeighbours(model,col,row,property,val,[1,4]) }
  750. /** getNeighbours for the Moore8 neighbourhood (range 1-8 in function getNeighbours) */
  751. getNeumann5(model, col, row, property,val) { return this.getNeighbours(model,col,row,property,val,[0,4]) }
  752. getNeighbours5(model, col, row, property,val) { return this.getNeighbours(model,col,row,property,val,[0,4]) }
  753. /** From a list of grid points, e.g. from getNeighbours(), sample one weighted by a property. This is analogous
  754. * to spinning a "roulette wheel". Also see a hard-coded versino of this in the "cheater" example
  755. * @param {Array} gps Array of gps to sample from (e.g. living individuals in neighbourhood)
  756. * @param {string} property The property used to weigh gps (e.g. fitness)
  757. * @param {float} non Scales the probability of not returning any gp.
  758. */
  759. rouletteWheel(gps, property, non = 0.0) {
  760. let sum_property = non;
  761. for (let i = 0; i < gps.length; i++) sum_property += gps[i][property]; // Now we have the sum of weight + a constant (non)
  762. let randomnr = this.rng.genrand_real1() * sum_property; // Sample a randomnr between 0 and sum_property
  763. let cumsum = 0.0; // This will keep track of the cumulative sum of weights
  764. for (let i = 0; i < gps.length; i++) {
  765. cumsum += gps[i][property];
  766. if (randomnr < cumsum) return gps[i]
  767. }
  768. return
  769. }
  770. /** Sum the properties of grid points in the neighbourhood (Neu4, Neu5, Moore8, Moore9 depending on range-array)
  771. * @param {GridModel} grid The gridmodel used to check neighbours. Usually the gridmodel itself (i.e., this),
  772. * but can be mixed to make grids interact.
  773. * @param {int} col position (column) for the focal gridpoint
  774. * @param {int} row position (row) for the focal gridpoint
  775. * @param {string} property the property that is counted
  776. * @param {Array} range which section of the neighbourhood must be counted? (see this.moore, e.g. 1-8 is Moore8, 0-4 is Neu5,etc)
  777. * @return {int} The number of grid points with "property" set to "val"
  778. * Below, 4 version of this functions are overloaded (Moore8, Moore9, Neumann4, etc.)
  779. * For example, if one wants to sum all the "fitness" surrounding a gridpoint in the Neumann neighbourhood, use
  780. * this.sumNeighbours(this,10,10,'fitness',[1-4]);
  781. * or
  782. * this.sumNeumann4(this,10,10,'fitness')
  783. */
  784. sumNeighbours(model, col, row, property, range) {
  785. let count = 0;
  786. for (let n = range[0]; n <= range[1]; n++) {
  787. let x = model.moore[n][0];
  788. let y = model.moore[n][1];
  789. let gp = model.getGridpoint(col + x, row + y);
  790. if(gp !== undefined && gp[property] !== undefined) count += gp[property];
  791. }
  792. return count;
  793. }
  794. /** sumNeighbours for range 1-8 (see sumNeighbours) */
  795. sumMoore8(grid, col, row, property) { return this.sumNeighbours(grid, col, row, property, [1,8]) }
  796. sumNeighbours8(grid, col, row, property) { return this.sumNeighbours(grid, col, row, property, [1,8]) }
  797. /** sumNeighbours for range 0-8 (see sumNeighbours) */
  798. sumMoore9(grid, col, row, property) { return this.sumNeighbours(grid, col, row, property, [0,8]) }
  799. sumNeighbours9(grid, col, row, property) { return this.sumNeighbours(grid, col, row, property, [0,8]) }
  800. /** sumNeighbours for range 1-4 (see sumNeighbours) */
  801. sumNeumann4(grid, col, row, property) { return this.sumNeighbours(grid, col, row, property, [1,4]) }
  802. sumNeighbours4(grid, col, row, property) { return this.sumNeighbours(grid, col, row, property, [1,4]) }
  803. /** sumNeighbours for range 0-4 (see sumNeighbours) */
  804. sumNeumann5(grid, col, row, property) { return this.sumNeighbours(grid, col, row, property, [0,4]) }
  805. sumNeighbours5(grid, col, row, property) { return this.sumNeighbours(grid, col, row, property, [0,4]) }
  806. /** Count the number of neighbours with 'val' in 'property' (Neu4, Neu5, Moore8, Moore9 depending on range-array)
  807. * @param {GridModel} grid The gridmodel used to check neighbours. Usually the gridmodel itself (i.e., this),
  808. * but can be mixed to make grids interact.
  809. * @param {int} col position (column) for the focal gridpoint
  810. * @param {int} row position (row) for the focal gridpoint
  811. * @param {string} property the property that is counted
  812. * @param {int} val value property must have to be counted
  813. * @param {Array} range which section of the neighbourhood must be counted? (see this.moore, e.g. 1-8 is Moore8, 0-4 is Neu5,etc)
  814. * @return {int} The number of grid points with "property" set to "val"
  815. * Below, 4 version of this functions are overloaded (Moore8, Moore9, Neumann4, etc.)
  816. * For example, if one wants to count all the "alive" individuals in the Moore 9 neighbourhood, use
  817. * this.countNeighbours(this,10,10,1,'alive',[0-8]);
  818. * or
  819. * this.countMoore9(this,10,10,1,'alive');
  820. */
  821. countNeighbours(model, col, row, property, val, range) {
  822. let count = 0;
  823. for (let n = range[0]; n <= range[1]; n++) {
  824. let x = model.moore[n][0];
  825. let y = model.moore[n][1];
  826. let neigh = model.getGridpoint(col + x, row + y);
  827. if (neigh !== undefined && neigh[property]==val) count++;
  828. }
  829. return count;
  830. }
  831. /** countNeighbours for range 1-8 (see countNeighbours) */
  832. countMoore8(model, col, row, property, val) { return this.countNeighbours(model, col, row, property, val, [1,8]) }
  833. countNeighbours8(model, col, row, property, val) { return this.countNeighbours(model, col, row, property, val, [1,8]) }
  834. /** countNeighbours for range 0-8 (see countNeighbours) */
  835. countMoore9(model, col, row, property, val) { return this.countNeighbours(model, col, row, property, val, [0,8]) }
  836. countNeighbours9(model, col, row, property, val) { return this.countNeighbours(model, col, row, property, val, [0,8]) }
  837. /** countNeighbours for range 1-4 (see countNeighbours) */
  838. countNeumann4(model, col, row, property, val) { return this.countNeighbours(model, col, row, property, val, [1,4]) }
  839. countNeighbours4(model, col, row, property, val) { return this.countNeighbours(model, col, row, property, val, [1,4]) }
  840. /** countNeighbours for range 0-4 (see countNeighbours) */
  841. countNeumann5(model, col, row, property, val) { return this.countNeighbours(model, col, row, property, val, [0,4]) }
  842. countNeighbours5(model, col, row, property, val) { return this.countNeighbours(model, col, row, property, val, [0,4]) }
  843. /** Return a random neighbour from the neighbourhood defined by range array
  844. * @param {GridModel} grid The gridmodel used to check neighbours. Usually the gridmodel itself (i.e., this),
  845. * but can be mixed to make grids interact.
  846. * @param {int} col position (column) for the focal gridpoint
  847. * @param {int} row position (row) for the focal gridpoint
  848. * @param {Array} range from which to sample (1-8 is Moore8, 0-4 is Neu5, etc.)
  849. */
  850. randomNeighbour(grid, col, row,range) {
  851. let rand = this.rng.genrand_int(range[0], range[1]);
  852. let x = this.moore[rand][0];
  853. let y = this.moore[rand][1];
  854. let neigh = grid.getGridpoint(col + x, row + y);
  855. while (neigh == undefined) neigh = this.randomNeighbour(grid, col, row,range);
  856. return neigh
  857. }
  858. /** randomMoore for range 1-8 (see randomMoore) */
  859. randomMoore8(model, col, row) { return this.randomNeighbour(model, col, row, [1,8]) }
  860. randomNeighbour8(model, col, row) { return this.randomNeighbour(model, col, row, [1,8]) }
  861. /** randomMoore for range 0-8 (see randomMoore) */
  862. randomMoore9(model, col, row) { return this.randomNeighbour(model, col, row, [0,8]) }
  863. randomNeighbour9(model, col, row) { return this.randomNeighbour(model, col, row, [0,8]) }
  864. /** randomMoore for range 1-4 (see randomMoore) */
  865. randomNeumann4(model, col, row) { return this.randomNeighbour(model, col, row, [1,4]) }
  866. randomNeighbour4(model, col, row) { return this.randomNeighbour(model, col, row, [1,4]) }
  867. /** randomMoore for range 0-4 (see randomMoore) */
  868. randomNeumann5(model, col, row) { return this.randomNeighbour(model, col, row, [0,4]) }
  869. randomNeighbour5(model, col, row) { return this.randomNeighbour(model, col, row, [0,4]) }
  870. /** Diffuse continuous states on the grid.
  871. * * @param {string} state The name of the state to diffuse
  872. * but can be mixed to make grids interact.
  873. * @param {float} rate the rate of diffusion. (<0.25)
  874. */
  875. diffuseStates(state,rate) {
  876. if(rate > 0.25) {
  877. throw new Error("Cacatoo: rate for diffusion cannot be greater than 0.25, try multiple diffusion steps instead.")
  878. }
  879. let newstate = MakeGrid(this.nc, this.nr, this.grid);
  880. for (let x = 0; x < this.nc; x += 1) // every column
  881. {
  882. for (let y = 0; y < this.nr; y += 1) // every row
  883. {
  884. for (let n = 1; n <= 4; n++) // Every neighbour (neumann)
  885. {
  886. let moore = this.moore[n];
  887. let xy = this.getNeighXY(x + moore[0], y + moore[1]);
  888. if (typeof xy == "undefined") continue
  889. let neigh = this.grid[xy[0]][xy[1]];
  890. newstate[x][y][state] += neigh[state] * rate;
  891. newstate[xy[0]][xy[1]][state] -= neigh[state] * rate;
  892. }
  893. }
  894. }
  895. for (let x = 0; x < this.nc; x += 1) // every column
  896. for (let y = 0; y < this.nr; y += 1) // every row
  897. this.grid[x][y][state] = newstate[x][y][state];
  898. }
  899. /** Diffuse continuous states on the grid.
  900. * * @param {string} state The name of the state to diffuse
  901. * but can be mixed to make grids interact.
  902. * @param {float} rate the rate of diffusion. (<0.25)
  903. */
  904. diffuseStateVector(statevector,rate) {
  905. if(rate > 0.25) {
  906. throw new Error("Cacatoo: rate for diffusion cannot be greater than 0.25, try multiple diffusion steps instead.")
  907. }
  908. let newstate = MakeGrid(this.nc, this.nr);
  909. // console.log(this.grid[0][0][statevector])
  910. for (let x = 0; x < this.nc; x += 1) // every column
  911. for (let y = 0; y < this.nr; y += 1) // every row
  912. {
  913. newstate[x][y][statevector] = Array(this.grid[x][y][statevector].length).fill(0);
  914. for (let n = 1; n <= 4; n++)
  915. for(let state of Object.keys(this.grid[x][y][statevector]))
  916. newstate[x][y][statevector][state] = this.grid[x][y][statevector][state];
  917. }
  918. for (let x = 0; x < this.nc; x += 1) // every column
  919. {
  920. for (let y = 0; y < this.nr; y += 1) // every row
  921. {
  922. for (let n = 1; n <= 4; n++) // Every neighbour (neumann)
  923. {
  924. let moore = this.moore[n];
  925. let xy = this.getNeighXY(x + moore[0], y + moore[1]);
  926. if (typeof xy == "undefined") continue
  927. let neigh = this.grid[xy[0]][xy[1]];
  928. for(let state of Object.keys(this.grid[x][y][statevector]))
  929. {
  930. newstate[x][y][statevector][state] += neigh[statevector][state] * rate;
  931. newstate[xy[0]][xy[1]][statevector][state] -= neigh[statevector][state] * rate;
  932. }
  933. }
  934. }
  935. }
  936. for (let x = 0; x < this.nc; x += 1) // every column
  937. for (let y = 0; y < this.nr; y += 1) // every row
  938. for (let n = 1; n <= 4; n++)
  939. for(let state of Object.keys(this.grid[x][y][statevector]))
  940. this.grid[x][y][statevector][state] = newstate[x][y][statevector][state];
  941. // console.log(this.grid)
  942. }
  943. /** Diffuse ODE states on the grid. Because ODEs are stored by reference inside gridpoint, the
  944. * states of the ODEs have to be first stored (copied) into a 4D array (x,y,ODE,state-vector),
  945. * which is then used to update the grid.
  946. */
  947. diffuseODEstates() {
  948. let newstates_2 = CopyGridODEs(this.nc, this.nr, this.grid); // Generates a 4D array of [x][y][o][s] (x-coord,y-coord,relevant ode,state-vector)
  949. for (let x = 0; x < this.nc; x += 1) // every column
  950. {
  951. for (let y = 0; y < this.nr; y += 1) // every row
  952. {
  953. for (let o = 0; o < this.grid[x][y].ODEs.length; o++) // every ode
  954. {
  955. for (let s = 0; s < this.grid[x][y].ODEs[o].state.length; s++) // every state
  956. {
  957. let rate = this.grid[x][y].ODEs[o].diff_rates[s];
  958. let sum_in = 0.0;
  959. for (let n = 1; n <= 4; n++) // Every neighbour (neumann)
  960. {
  961. let moore = this.moore[n];
  962. let xy = this.getNeighXY(x + moore[0], y + moore[1]);
  963. if (typeof xy == "undefined") continue
  964. let neigh = this.grid[xy[0]][xy[1]];
  965. sum_in += neigh.ODEs[o].state[s] * rate;
  966. newstates_2[xy[0]][xy[1]][o][s] -= neigh.ODEs[o].state[s] * rate;
  967. }
  968. newstates_2[x][y][o][s] += sum_in;
  969. }
  970. }
  971. }
  972. }
  973. for (let x = 0; x < this.nc; x += 1) // every column
  974. for (let y = 0; y < this.nr; y += 1) // every row
  975. for (let o = 0; o < this.grid[x][y].ODEs.length; o++)
  976. for (let s = 0; s < this.grid[x][y].ODEs[o].state.length; s++)
  977. this.grid[x][y].ODEs[o].state[s] = newstates_2[x][y][o][s];
  978. }
  979. /** Assign each gridpoint a new random position on the grid. This simulated mixing,
  980. * but does not guarantee a "well-mixed" system per se (interactions are still local)
  981. * calculated based on neighbourhoods.
  982. */
  983. perfectMix() {
  984. let all_gridpoints = [];
  985. for (let x = 0; x < this.nc; x++)
  986. for (let y = 0; y < this.nr; y++)
  987. all_gridpoints.push(this.getGridpoint(x, y));
  988. all_gridpoints = shuffle(all_gridpoints, this.rng);
  989. for (let x = 0; x < all_gridpoints.length; x++)
  990. this.setGridpoint(x % this.nc, Math.floor(x / this.nc), all_gridpoints[x]);
  991. return "Perfectly mixed the grid"
  992. }
  993. /** Apply diffusion algorithm for grid-based models described in Toffoli & Margolus' book "Cellular automata machines"
  994. * The idea is to subdivide the grid into 2x2 neighbourhoods, and rotate them (randomly CW or CCW). To avoid particles
  995. * simply being stuck in their own 2x2 subspace, different 2x2 subspaces are taken each iteration (CW in even iterations,
  996. * CCW in odd iterations)
  997. */
  998. MargolusDiffusion() {
  999. //
  1000. // A B
  1001. // D C
  1002. // a = backup of A
  1003. // rotate cw or ccw randomly
  1004. let even = this.margolus_phase % 2 == 0;
  1005. if ((this.nc % 2 + this.nr % 2) > 0) throw "Do not use margolusDiffusion with an uneven number of cols / rows!"
  1006. for (let x = 0 + even; x < this.nc; x += 2) {
  1007. if(x> this.nc-2) continue
  1008. for (let y = 0 + even; y < this.nr; y += 2) {
  1009. if(y> this.nr-2) continue
  1010. // console.log(x,y)
  1011. let old_A = new Gridpoint(this.grid[x][y]);
  1012. let A = this.getGridpoint(x, y);
  1013. let B = this.getGridpoint(x + 1, y);
  1014. let C = this.getGridpoint(x + 1, y + 1);
  1015. let D = this.getGridpoint(x, y + 1);
  1016. if (this.rng.random() < 0.5) // CW = clockwise rotation
  1017. {
  1018. A = D;
  1019. D = C;
  1020. C = B;
  1021. B = old_A;
  1022. }
  1023. else {
  1024. A = B; // CCW = counter clockwise rotation
  1025. B = C;
  1026. C = D;
  1027. D = old_A;
  1028. }
  1029. this.setGridpoint(x, y, A);
  1030. this.setGridpoint(x + 1, y, B);
  1031. this.setGridpoint(x + 1, y + 1, C);
  1032. this.setGridpoint(x, y + 1, D);
  1033. }
  1034. }
  1035. this.margolus_phase++;
  1036. }
  1037. /**
  1038. * Adds a dygraph-plot to your DOM (if the DOM is loaded)
  1039. * @param {Array} graph_labels Array of strings for the graph legend
  1040. * @param {Array} graph_values Array of floats to plot (here plotted over time)
  1041. * @param {Array} cols Array of colours to use for plotting
  1042. * @param {String} title Title of the plot
  1043. * @param {Object} opts dictionary-style list of opts to pass onto dygraphs
  1044. */
  1045. plotArray(graph_labels, graph_values, cols, title, opts) {
  1046. if (typeof window == 'undefined') return
  1047. if (!(title in this.graphs)) {
  1048. cols = parseColours(cols);
  1049. graph_values.unshift(this.time);
  1050. graph_labels.unshift("Time");
  1051. this.graphs[title] = new Graph(graph_labels, graph_values, cols, title, opts);
  1052. }
  1053. else {
  1054. if (this.time % this.graph_interval == 0) {
  1055. graph_values.unshift(this.time);
  1056. graph_labels.unshift("Time");
  1057. this.graphs[title].push_data(graph_values);
  1058. }
  1059. if (this.time % this.graph_update == 0) {
  1060. this.graphs[title].update();
  1061. }
  1062. }
  1063. }
  1064. /**
  1065. * Adds a dygraph-plot to your DOM (if the DOM is loaded)
  1066. * @param {Array} graph_values Array of floats to plot (here plotted over time)
  1067. * @param {String} title Title of the plot
  1068. * @param {Object} opts dictionary-style list of opts to pass onto dygraphs
  1069. */
  1070. plotPoints(graph_values, title, opts) {
  1071. let graph_labels = Array.from({length: graph_values.length}, (v, i) => 'sample'+(i+1));
  1072. let cols = Array.from({length: graph_values.length}, (v, i) => 'black');
  1073. let seriesname = 'average';
  1074. let sum = 0;
  1075. let num = 0;
  1076. // Get average of all defined values
  1077. for(let n = 0; n< graph_values.length; n++){
  1078. if(graph_values[n] !== undefined) {
  1079. sum += graph_values[n];
  1080. num++;
  1081. }
  1082. }
  1083. let avg = (sum / num) || 0;
  1084. graph_values.unshift(avg);
  1085. graph_labels.unshift(seriesname);
  1086. cols.unshift("#666666");
  1087. if(opts == undefined) opts = {};
  1088. opts.drawPoints = true;
  1089. opts.strokeWidth = 0;
  1090. opts.pointSize = 1;
  1091. opts.series = {[seriesname]: {strokeWidth: 3.0, strokeColor:"green", drawPoints: false, pointSize: 0, highlightCircleSize: 3 }};
  1092. if (typeof window == 'undefined') return
  1093. if (!(title in this.graphs)) {
  1094. cols = parseColours(cols);
  1095. graph_values.unshift(this.time);
  1096. graph_labels.unshift("Time");
  1097. this.graphs[title] = new Graph(graph_labels, graph_values, cols, title, opts);
  1098. }
  1099. else {
  1100. if (this.time % this.graph_interval == 0) {
  1101. graph_values.unshift(this.time);
  1102. graph_labels.unshift("Time");
  1103. this.graphs[title].push_data(graph_values);
  1104. }
  1105. if (this.time % this.graph_update == 0) {
  1106. this.graphs[title].update();
  1107. }
  1108. }
  1109. }
  1110. /**
  1111. * Adds a dygraph-plot to your DOM (if the DOM is loaded)
  1112. * @param {Array} graph_labels Array of strings for the graph legend
  1113. * @param {Array} graph_values Array of 2 floats to plot (first value for x-axis, second value for y-axis)
  1114. * @param {Array} cols Array of colours to use for plotting
  1115. * @param {String} title Title of the plot
  1116. * @param {Object} opts dictionary-style list of opts to pass onto dygraphs
  1117. */
  1118. plotXY(graph_labels, graph_values, cols, title, opts) {
  1119. if (typeof window == 'undefined') return
  1120. if (!(title in this.graphs)) {
  1121. cols = parseColours(cols);
  1122. this.graphs[title] = new Graph(graph_labels, graph_values, cols, title, opts);
  1123. }
  1124. else {
  1125. if (this.time % this.graph_interval == 0) {
  1126. this.graphs[title].push_data(graph_values);
  1127. }
  1128. if (this.time % this.graph_update == 0) {
  1129. this.graphs[title].update();
  1130. }
  1131. }
  1132. }
  1133. /**
  1134. * Easy function to add a pop-sizes plot (wrapper for plotArrays)
  1135. * @param {String} property What property to plot (needs to exist in your model, e.g. "species" or "alive")
  1136. * @param {Array} values Which values are plotted (e.g. [1,3,4,6])
  1137. */
  1138. plotPopsizes(property, values, opts) {
  1139. if (typeof window == 'undefined') return
  1140. if (this.time % this.graph_interval != 0 && this.graphs[`Population sizes (${this.name})`] !== undefined) return
  1141. // Wrapper for plotXY function, which expects labels, values, colours, and a title for the plot:
  1142. // Labels
  1143. let graph_labels = [];
  1144. for (let val of values) { graph_labels.push(property + '_' + val); }
  1145. // Values
  1146. let popsizes = this.getPopsizes(property, values);
  1147. let graph_values = popsizes;
  1148. // Colours
  1149. let colours = [];
  1150. for (let c of values) {
  1151. if (this.statecolours[property].constructor != Object)
  1152. colours.push(this.statecolours[property]);
  1153. else
  1154. colours.push(this.statecolours[property][c]);
  1155. }
  1156. // Title
  1157. let title = "Population sizes (" + this.name + ")";
  1158. if(opts && opts.title) title = opts.title;
  1159. this.plotArray(graph_labels, graph_values, colours, title, opts);
  1160. //this.graph = new Graph(graph_labels,graph_values,colours,"Population sizes ("+this.name+")")
  1161. }
  1162. /**
  1163. * Easy function to add a ODE states (wrapper for plot array)
  1164. * @param {String} ODE name Which ODE to plot the states for
  1165. * @param {Array} values Which states are plotted (if undefined, all of them are plotted)
  1166. */
  1167. plotODEstates(odename, values, colours) {
  1168. if (typeof window == 'undefined') return
  1169. if (this.time % this.graph_interval != 0 && this.graphs[`Average ODE states (${this.name})`] !== undefined) return
  1170. // Labels
  1171. let graph_labels = [];
  1172. for (let val of values) { graph_labels.push(odename + '_' + val); }
  1173. // Values
  1174. let ode_states = this.getODEstates(odename, values);
  1175. // Title
  1176. let title = "Average ODE states (" + this.name + ")";
  1177. this.plotArray(graph_labels, ode_states, colours, title);
  1178. }
  1179. drawSlide(canvasname,prefix="grid_") {
  1180. let canvas = this.canvases[canvasname].elem; // Grab the canvas element
  1181. let timestamp = sim.time.toString();
  1182. timestamp = timestamp.padStart(5, "0");
  1183. canvas.toBlob(function(blob)
  1184. {
  1185. saveAs(blob, prefix+timestamp+".png");
  1186. });
  1187. }
  1188. resetPlots() {
  1189. this.time = 0;
  1190. for (let g in this.graphs) {
  1191. this.graphs[g].reset_plot();
  1192. }
  1193. }
  1194. /**
  1195. * Returns an array with the population sizes of different types
  1196. * @param {String} property Return popsizes for this property (needs to exist in your model, e.g. "species" or "alive")
  1197. * @param {Array} values Which values are counted and returned (e.g. [1,3,4,6])
  1198. */
  1199. getPopsizes(property, values) {
  1200. let sum = Array(values.length).fill(0);
  1201. for (let x = 0; x < this.nc; x++) {
  1202. for (let y = 0; y < this.nr; y++) {
  1203. for (let val in values)
  1204. if (this.grid[x][y][property] == values[val]) sum[val]++;
  1205. }
  1206. }
  1207. return sum;
  1208. }
  1209. /**
  1210. * Returns an array with the population sizes of different types
  1211. * @param {String} property Return popsizes for this property (needs to exist in your model, e.g. "species" or "alive")
  1212. * @param {Array} values Which values are counted and returned (e.g. [1,3,4,6])
  1213. */
  1214. getODEstates(odename, values) {
  1215. let sum = Array(values.length).fill(0);
  1216. for (let x = 0; x < this.nc; x++)
  1217. for (let y = 0; y < this.nr; y++)
  1218. for (let val in values)
  1219. sum[val] += this.grid[x][y][odename].state[val] / (this.nc * this.nr);
  1220. return sum;
  1221. }
  1222. /**
  1223. * Attaches an ODE to all GPs in the model. Each gridpoint has it's own ODE.
  1224. * @param {function} eq Function that describes the ODEs, see examples starting with "ode"
  1225. * @param {Object} conf dictionary style configuration of your ODEs (initial state, parameters, etc.)
  1226. */
  1227. attachODE(eq, conf) {
  1228. for (let x = 0; x < this.nc; x++) {
  1229. for (let y = 0; y < this.nr; y++) {
  1230. let ode = new ODE(eq, conf.init_states, conf.parameters, conf.diffusion_rates, conf.ode_name, conf.acceptable_error);
  1231. if (typeof this.grid[x][y].ODEs == "undefined") this.grid[x][y].ODEs = []; // If list doesnt exist yet
  1232. this.grid[x][y].ODEs.push(ode);
  1233. if (conf.ode_name) this.grid[x][y][conf.ode_name] = ode;
  1234. }
  1235. }
  1236. }
  1237. /**
  1238. * Numerically solve the ODEs for each grid point
  1239. * @param {float} delta_t Step size
  1240. * @param {bool} opt_pos When enabled, negative values are set to 0 automatically
  1241. */
  1242. solveAllODEs(delta_t = 0.1, opt_pos = false) {
  1243. for (let x = 0; x < this.nc; x++) {
  1244. for (let y = 0; y < this.nr; y++) {
  1245. for (let ode of this.grid[x][y].ODEs) {
  1246. ode.solveTimestep(delta_t, opt_pos);
  1247. }
  1248. }
  1249. }
  1250. }
  1251. /**
  1252. * Print the entire grid to the console. Not always recommended, but useful for debugging
  1253. * @param {float} property What property is printed
  1254. * @param {float} fract Subset to be printed (from the top-left)
  1255. */
  1256. printGrid(property, fract) {
  1257. let ncol = this.nc;
  1258. let nrow = this.nr;
  1259. if (fract != undefined) ncol *= fract, nrow *= fract;
  1260. let grid = new Array(nrow); // Makes a column or <rows> long --> grid[cols]
  1261. for (let x = 0; x < ncol; x++)
  1262. grid[x] = new Array(ncol); // Insert a row of <cols> long --> grid[cols][rows]
  1263. for (let y = 0; y < nrow; y++)
  1264. grid[x][y] = this.grid[x][y][property];
  1265. console.table(grid);
  1266. }
  1267. }
  1268. ////////////////////////////////////////////////////////////////////////////////////////////////////
  1269. // The functions below are not methods of grid-model as they are never unique for a particular model.
  1270. ////////////////////////////////////////////////////////////////////////////////////////////////////
  1271. /**
  1272. * Make a grid, or when a template is given, a COPY of a grid.
  1273. * @param {int} cols Width of the new grid
  1274. * @param {int} rows Height of the new grid
  1275. * @param {2DArray} template Template to be used for copying (if not set, a new empty grid is made)
  1276. */
  1277. let MakeGrid = function(cols, rows, template) {
  1278. let grid = new Array(rows); // Makes a column or <rows> long --> grid[cols]
  1279. for (let x = 0; x < cols; x++) {
  1280. grid[x] = new Array(cols); // Insert a row of <cols> long --> grid[cols][rows]
  1281. for (let y = 0; y < rows; y++) {
  1282. if (template) grid[x][y] = new Gridpoint(template[x][y]); // Make a deep or shallow copy of the GP
  1283. else grid[x][y] = new Gridpoint();
  1284. }
  1285. }
  1286. return grid;
  1287. };
  1288. /**
  1289. * Make a back-up of all the ODE states (for synchronous ODE updating)
  1290. * @param {int} cols Width of the grid
  1291. * @param {int} rows Height of the grid
  1292. * @param {2DArray} template Get ODE states from here
  1293. */
  1294. let CopyGridODEs = function(cols, rows, template) {
  1295. let grid = new Array(rows); // Makes a column or <rows> long --> grid[cols]
  1296. for (let x = 0; x < cols; x++) {
  1297. grid[x] = new Array(cols); // Insert a row of <cols> long --> grid[cols][rows]
  1298. for (let y = 0; y < rows; y++) {
  1299. for (let o = 0; o < template[x][y].ODEs.length; o++) // every ode
  1300. {
  1301. grid[x][y] = [];
  1302. let states = [];
  1303. for (let s = 0; s < template[x][y].ODEs[o].state.length; s++) // every state
  1304. states.push(template[x][y].ODEs[o].state[s]);
  1305. grid[x][y][o] = states;
  1306. }
  1307. }
  1308. }
  1309. return grid;
  1310. };
  1311. /**
  1312. * Quadtrees is a hierarchical data structure to quickly look up boids in flocking models to speed up the simulation
  1313. */
  1314. class QuadTree {
  1315. constructor(boundary, capacity) {
  1316. this.boundary = boundary; // Object with x, y coordinates and a width (w) and height (h)
  1317. this.capacity = capacity; // How many boids fit in this Quadrant until it divides in 4 more quadrants
  1318. this.points = []; // Points contain the boids (object with x and y position)
  1319. this.divided = false; // Boolean to check if this Quadrant is futher divided
  1320. }
  1321. // Method to subdivide the current Quadtree into four equal quadrants
  1322. subdivide() {
  1323. let {x,y,w,h} = this.boundary;
  1324. let nw = { x: x-w/4, y: y-h/4, w: w/2, h: h/2 };
  1325. let ne = { x: x+w/4, y: y-h/4, w: w/2, h: h/2 };
  1326. let sw = { x: x-w/4, y: y+h/4, w: w/2, h: h/2 };
  1327. let se = { x: x+w/4, y: y+h/4, w: w/2, h: h/2 };
  1328. this.northwest = new QuadTree(nw, this.capacity);
  1329. this.northeast = new QuadTree(ne, this.capacity);
  1330. this.southwest = new QuadTree(sw, this.capacity);
  1331. this.southeast = new QuadTree(se, this.capacity);
  1332. this.divided = true; // Subdivisions are not divided when spawned, but this one is.
  1333. }
  1334. // Insert a point into the quadtree to query it later (! recursive)
  1335. insert(point) {
  1336. // If this point doesn't belong here, return false
  1337. if (!this.contains(this.boundary, point.position)) {
  1338. return false
  1339. }
  1340. // If the capacity is not yet reached, add the point and return true
  1341. if (this.points.length < this.capacity) {
  1342. this.points.push(point);
  1343. return true;
  1344. }
  1345. // Capacity is reached, divide the quadrant
  1346. if (!this.divided) {
  1347. this.subdivide();
  1348. }
  1349. // Try and insert in one of the subquadrants, and return true if one is succesful (here is the recursion)
  1350. if (this.northwest.insert(point) || this.northeast.insert(point) ||
  1351. this.southwest.insert(point) || this.southeast.insert(point)) {
  1352. return true
  1353. }
  1354. return false
  1355. }
  1356. // Test if a point is within a rectangle
  1357. contains(rect, point) {
  1358. return !(point.x < rect.x - rect.w/2 || point.x > rect.x + rect.w/2 ||
  1359. point.y < rect.y - rect.h/2 || point.y > rect.y + rect.h/2)
  1360. }
  1361. // Query, another recursive function
  1362. query(range, found) {
  1363. // If there are no points yet, make a list of points
  1364. if (!found) found = [];
  1365. // If it doesn't intersect, return whatever was found so far and move on
  1366. if (!this.intersects(this.boundary, range)) {
  1367. return found
  1368. }
  1369. // Check for all points if it is in this quadtree (could also be in one of the children QTs!)
  1370. for (let p of this.points) {
  1371. if (this.contains(range, p.position)) {
  1372. found.push(p);
  1373. }
  1374. }
  1375. // Test the children QTs too (here is the recursion!)
  1376. if (this.divided) {
  1377. this.northwest.query(range, found);
  1378. this.northeast.query(range, found);
  1379. this.southwest.query(range, found);
  1380. this.southeast.query(range, found);
  1381. }
  1382. // Done, return everything that was found.
  1383. return found;
  1384. }
  1385. // Check if two rectangles are intersecting (usually query rectangle vs quadtree boundary)
  1386. intersects(rect1, rect2) {
  1387. return !(rect2.x - rect2.w / 2 > rect1.x + rect1.w / 2 ||
  1388. rect2.x + rect2.w / 2 < rect1.x - rect1.w / 2 ||
  1389. rect2.y - rect2.h / 2 > rect1.y + rect1.h / 2 ||
  1390. rect2.y + rect2.h / 2 < rect1.y - rect1.h / 2);
  1391. }
  1392. // Draw the qt on the provided ctx
  1393. draw(ctx,scale,col) {
  1394. ctx.strokeStyle = col;
  1395. ctx.lineWidth = 1;
  1396. ctx.strokeRect(this.boundary.x*scale - this.boundary.w*scale / 2, this.boundary.y*scale - this.boundary.h*scale / 2, this.boundary.w*scale, this.boundary.h*scale);
  1397. if (this.divided) {
  1398. this.northwest.draw(ctx,scale);
  1399. this.northeast.draw(ctx,scale);
  1400. this.southwest.draw(ctx,scale);
  1401. this.southeast.draw(ctx,scale);
  1402. }
  1403. }
  1404. }
  1405. /**
  1406. * Flockmodel is the second modeltype in Cacatoo, which uses Boids that can interact with a @Gridmodel
  1407. */
  1408. class Flockmodel {
  1409. /**
  1410. * The constructor function for a @Flockmodl object. Takes the same config dictionary as used in @Simulation
  1411. * @param {string} name The name of your model. This is how it will be listed in @Simulation 's properties
  1412. * @param {dictionary} config A dictionary (object) with all the necessary settings to setup a Cacatoo GridModel.
  1413. * @param {MersenneTwister} rng A random number generator (MersenneTwister object)
  1414. */
  1415. constructor(name, config={}, rng) {
  1416. this.name = name;
  1417. this.config = config;
  1418. this.time = 0;
  1419. this.draw = true;
  1420. this.max_force = config.max_force || 1;
  1421. this.max_speed = config.max_speed || 1;
  1422. this.width = config.width || config.ncol ||600;
  1423. this.height = config.height ||config.nrow || 600;
  1424. this.scale = config.scale || 1;
  1425. this.shape = config.shape || 'dot';
  1426. this.click = config.click || 'none';
  1427. this.follow_mouse = config.follow_mouse;
  1428. this.init_velocity = config.init_velocity || 0.1;
  1429. this.rng = rng;
  1430. this.random = () => { return this.rng.random()};
  1431. this.randomInt = (a,b) => { return this.rng.randomInt(a,b)};
  1432. this.wrap = config.wrap || [true, true];
  1433. this.wrapreflect = 1;
  1434. if(config.wrapreflect) this.wrapreflect = config.wrapreflect;
  1435. this.graph_update = config.graph_update || 20;
  1436. this.graph_interval = config.graph_interval || 2;
  1437. this.bgcolour = config.bgcolour || undefined;
  1438. this.physics = true;
  1439. if(config.physics && config.physics != true) this.physics = false;
  1440. console.log(config);
  1441. this.statecolours = {};
  1442. if(config.statecolours) this.statecolours = this.setupColours(config.statecolours,config.num_colours||100); // Makes sure the statecolours in the config dict are parsed (see below)
  1443. if(!config.qt_capacity) config.qt_capacity = 3;
  1444. this.graphs = {}; // Object containing all graphs belonging to this model (HTML usage only)
  1445. this.canvases = {}; // Object containing all Canvases belonging to this model (HTML usage only)
  1446. // Flocking stuff
  1447. let radius_alignment = this.config.alignment ? this.config.alignment.radius : 0;
  1448. let radius_cohesion = this.config.cohesion ? this.config.cohesion.radius : 0;
  1449. let radius_separation = this.config.separation ? this.config.separation.radius : 0;
  1450. this.neighbourhood_radius = Math.max(radius_alignment,radius_cohesion,radius_separation);
  1451. this.friction = this.config.friction;
  1452. this.mouse_radius = this.config.mouse_radius || 100;
  1453. this.mousecoords = {x:-1000,y:-1000};
  1454. this.boids = [];
  1455. this.mouseboids = [];
  1456. this.obstacles = [];
  1457. this.populateSpot();
  1458. this.build_quadtree();
  1459. }
  1460. build_quadtree(){
  1461. let boundary = { x: this.width/2, y: this.height/2, w: this.width, h: this.height };
  1462. this.qt = new QuadTree(boundary, this.config.qt_capacity);
  1463. for (let boid of this.boids) {
  1464. this.qt.insert(boid);
  1465. }
  1466. }
  1467. /**
  1468. * Populates the space with individuals in a certain radius from the center
  1469. */
  1470. populateSpot(num,put_x,put_y,s){
  1471. let n = num || this.config.num_boids;
  1472. let size = s || this.width/2;
  1473. let x = put_x || this.width/2;
  1474. let y = put_y || this.height/2;
  1475. for(let i=0; i<n;i++){
  1476. let angle = this.random() * 2 * Math.PI;
  1477. this.boids.push({
  1478. position: { x: x + size - 2*this.random()*size, y: y+size-2*this.random()*size },
  1479. velocity: { x: this.init_velocity*Math.cos(angle) * this.max_speed, y: this.init_velocity*Math.sin(angle) * this.max_speed },
  1480. acceleration: { x: 0, y: 0 },
  1481. size: this.config.size
  1482. });
  1483. }
  1484. }
  1485. copyBoid(boid){
  1486. return copy(boid)
  1487. }
  1488. /** TODO
  1489. * Saves the current flock a JSON object
  1490. * @param {string} filename The name of of the JSON file
  1491. */
  1492. save_flock(filename)
  1493. {
  1494. }
  1495. /**
  1496. * Reads a JSON file and loads a JSON object onto this flockmodel. Reading a local JSON file will not work in browser.
  1497. * Gridmodels 'addCheckpointButton' instead, which may be implemented for flocks at a later stage.
  1498. * @param {string} file Path to the json file
  1499. */
  1500. load_flock(file)
  1501. {
  1502. }
  1503. /** Initiate a dictionary with colour arrays [R,G,B] used by Graph and Canvas classes
  1504. * @param {statecols} object - given object can be in two forms
  1505. * | either {state:colour} tuple (e.g. 'alive':'white', see gol.html)
  1506. * | or {state:object} where objects are {val:'colour},
  1507. * | e.g. {'species':{0:"black", 1:"#DDDDDD", 2:"red"}}, see cheater.html
  1508. */
  1509. setupColours(statecols,num_colours=18) {
  1510. let return_dict = {};
  1511. if (statecols == null){ // If the user did not define statecols (yet)
  1512. return return_dict["state"] = default_colours(num_colours)
  1513. }
  1514. let colours = dict_reverse(statecols) || { 'val': 1 };
  1515. for (const [statekey, statedict] of Object.entries(colours)) {
  1516. if (statedict == 'default') {
  1517. return_dict[statekey] = default_colours(num_colours+1);
  1518. }
  1519. else if (statedict == 'random') {
  1520. return_dict[statekey] = random_colours(num_colours+1,this.rng);
  1521. }
  1522. else if (statedict == 'viridis') {
  1523. let colours = this.colourGradientArray(num_colours, 0,[68, 1, 84], [59, 82, 139], [33, 144, 140], [93, 201, 99], [253, 231, 37]);
  1524. return_dict[statekey] = colours;
  1525. }
  1526. else if (statedict == 'inferno') {
  1527. let colours = this.colourGradientArray(num_colours, 0,[20, 11, 52], [132, 32, 107], [229, 92, 45], [246, 215, 70]);
  1528. return_dict[statekey] = colours;
  1529. }
  1530. else if (statedict == 'inferno_rev') {
  1531. let colours = this.colourGradientArray(num_colours, 0, [246, 215, 70], [229, 92, 45], [132, 32, 107]);
  1532. return_dict[statekey] = colours;
  1533. }
  1534. else if (typeof statedict === 'string' || statedict instanceof String) // For if
  1535. {
  1536. return_dict[statekey] = stringToRGB(statedict);
  1537. }
  1538. else {
  1539. let c = {};
  1540. for (const [key, val] of Object.entries(statedict)) {
  1541. if (Array.isArray(val)) c[key] = val;
  1542. else c[key] = stringToRGB(val);
  1543. }
  1544. return_dict[statekey] = c;
  1545. }
  1546. }
  1547. return return_dict
  1548. }
  1549. /** Initiate a gradient of colours for a property (return array only)
  1550. * @param {string} property The name of the property to which the colour is assigned
  1551. * @param {int} n How many colours the gradient consists off
  1552. * For example usage, see colourViridis below
  1553. */
  1554. colourGradientArray(n,total)
  1555. {
  1556. let color_dict = {};
  1557. //color_dict[0] = [0, 0, 0]
  1558. let n_arrays = arguments.length - 2;
  1559. if (n_arrays <= 1) throw new Error("colourGradient needs at least 2 arrays")
  1560. let segment_len = Math.ceil(n / (n_arrays-1));
  1561. if(n <= 10 && n_arrays > 3) console.warn("Cacatoo warning: forming a complex gradient with only few colours... hoping for the best.");
  1562. let total_added_colours = 0;
  1563. for (let arr = 0; arr < n_arrays - 1 ; arr++) {
  1564. let arr1 = arguments[2 + arr];
  1565. let arr2 = arguments[2 + arr + 1];
  1566. for (let i = 0; i < segment_len; i++) {
  1567. let r, g, b;
  1568. if (arr2[0] > arr1[0]) r = Math.floor(arr1[0] + (arr2[0] - arr1[0])*( i / (segment_len-1) ));
  1569. else r = Math.floor(arr1[0] - (arr1[0] - arr2[0]) * (i / (segment_len-1)));
  1570. if (arr2[1] > arr1[1]) g = Math.floor(arr1[1] + (arr2[1] - arr1[1]) * (i / (segment_len - 1)));
  1571. else g = Math.floor(arr1[1] - (arr1[1] - arr2[1]) * (i / (segment_len - 1)));
  1572. if (arr2[2] > arr1[2]) b = Math.floor(arr1[2] + (arr2[2] - arr1[2]) * (i / (segment_len - 1)));
  1573. else b = Math.floor(arr1[2] - (arr1[2] - arr2[2]) * (i / (segment_len - 1)));
  1574. color_dict[Math.floor(i + arr * segment_len + total)+1] = [Math.min(r,255), Math.min(g,255), Math.min(b,255)];
  1575. total_added_colours++;
  1576. if(total_added_colours == n) break
  1577. }
  1578. }
  1579. return(color_dict)
  1580. }
  1581. /** Initiate a gradient of colours for a property.
  1582. * @param {string} property The name of the property to which the colour is assigned
  1583. * @param {int} n How many colours the gradient consists off
  1584. * For example usage, see colourViridis below
  1585. */
  1586. colourGradient(property, n) {
  1587. let offset = 2;
  1588. let n_arrays = arguments.length - offset;
  1589. if (n_arrays <= 1) throw new Error("colourGradient needs at least 2 arrays")
  1590. let color_dict = {};
  1591. let total = 0;
  1592. if(this.statecolours !== undefined && this.statecolours[property] !== undefined){
  1593. color_dict = this.statecolours[property];
  1594. total = Object.keys(this.statecolours[property]).length;
  1595. }
  1596. let all_arrays = [];
  1597. for (let arr = 0; arr < n_arrays ; arr++) all_arrays.push(arguments[offset + arr]);
  1598. let new_dict = this.colourGradientArray(n,total,...all_arrays);
  1599. this.statecolours[property] = {...color_dict,...new_dict};
  1600. }
  1601. /** Initiate a gradient of colours for a property, using the Viridis colour scheme (purpleblue-ish to green to yellow) or Inferno (black to orange to yellow)
  1602. * @param {string} property The name of the property to which the colour is assigned
  1603. * @param {int} n How many colours the gradient consists off
  1604. * @param {bool} rev Reverse the viridis colour gradient
  1605. */
  1606. colourViridis(property, n, rev = false, option="viridis") {
  1607. if(option=="viridis"){
  1608. if (!rev) this.colourGradient(property, n, [68, 1, 84], [59, 82, 139], [33, 144, 140], [93, 201, 99], [253, 231, 37]); // Viridis
  1609. else this.colourGradient(property, n, [253, 231, 37], [93, 201, 99], [33, 144, 140], [59, 82, 139], [68, 1, 84]); // Viridis
  1610. }
  1611. else if(option=="inferno"){
  1612. if (!rev) this.colourGradient(property, n, [20, 11, 52], [132, 32, 107], [229, 92, 45], [246, 215, 70]); // Inferno
  1613. else this.colourGradient(property, n, [246, 215, 70], [229, 92, 45], [132, 32, 107], [20, 11, 52]); // Inferno
  1614. }
  1615. }
  1616. /** Flocking of individuals, based on X, Y, Z (TODO)
  1617. * @param {Object} i The individual to be updates
  1618. */
  1619. flock(){
  1620. if(this.physics) this.applyPhysics();
  1621. this.build_quadtree();
  1622. }
  1623. calculateAlignment(boid, neighbours, max_speed) {
  1624. let steering = { x: 0, y: 0 };
  1625. if (neighbours.length > 0) {
  1626. for (let neighbour of neighbours) {
  1627. steering.x += neighbour.velocity.x;
  1628. steering.y += neighbour.velocity.y;
  1629. }
  1630. steering.x /= neighbours.length;
  1631. steering.y /= neighbours.length;
  1632. steering = this.normaliseVector(steering);
  1633. steering.x *= max_speed;
  1634. steering.y *= max_speed;
  1635. steering.x -= boid.velocity.x;
  1636. steering.y -= boid.velocity.y;
  1637. }
  1638. return steering;
  1639. }
  1640. calculateSeparation(boid, neighbours, max_speed) {
  1641. let steering = { x: 0, y: 0 };
  1642. if (neighbours.length > 0) {
  1643. for (let neighbour of neighbours) {
  1644. let dx = boid.position.x - neighbour.position.x;
  1645. let dy = boid.position.y - neighbour.position.y;
  1646. // Adjust for wrapping in the x direction
  1647. if (Math.abs(dx) > this.width / 2) {
  1648. dx = dx - Math.sign(dx) * this.width;
  1649. }
  1650. // Adjust for wrapping in the y direction
  1651. if (Math.abs(dy) > this.height / 2) {
  1652. dy = dy - Math.sign(dy) * this.height;
  1653. }
  1654. let distance = Math.sqrt(dx * dx + dy * dy);
  1655. if (distance < this.config.separation.radius) {
  1656. let difference = { x: dx, y: dy };
  1657. difference = this.normaliseVector(difference);
  1658. steering.x += difference.x ;
  1659. steering.y += difference.y ;
  1660. }
  1661. }
  1662. if (steering.x !== 0 || steering.y !== 0) {
  1663. steering.x /= neighbours.length;
  1664. steering.y /= neighbours.length;
  1665. steering = this.normaliseVector(steering);
  1666. steering.x *= max_speed;
  1667. steering.y *= max_speed;
  1668. steering.x -= boid.velocity.x;
  1669. steering.y -= boid.velocity.y;
  1670. }
  1671. }
  1672. return steering;
  1673. }
  1674. calculateCohesion(boid, neighbours, max_speed) {
  1675. let steering = { x: 0, y: 0 };
  1676. if (neighbours.length > 0) {
  1677. let centerOfMass = { x: 0, y: 0 };
  1678. for (let neighbour of neighbours) {
  1679. let dx = neighbour.position.x - boid.position.x;
  1680. let dy = neighbour.position.y - boid.position.y;
  1681. // Adjust for wrapping in the x direction
  1682. if (Math.abs(dx) > this.width / 2) {
  1683. dx = dx - Math.sign(dx) * this.width;
  1684. }
  1685. // Adjust for wrapping in the y direction
  1686. if (Math.abs(dy) > this.height / 2) {
  1687. dy = dy - Math.sign(dy) * this.height;
  1688. }
  1689. centerOfMass.x += boid.position.x + dx;
  1690. centerOfMass.y += boid.position.y + dy;
  1691. }
  1692. centerOfMass.x /= neighbours.length;
  1693. centerOfMass.y /= neighbours.length;
  1694. steering.x = centerOfMass.x - boid.position.x;
  1695. steering.y = centerOfMass.y - boid.position.y;
  1696. steering = this.normaliseVector(steering);
  1697. steering.x *= max_speed;
  1698. steering.y *= max_speed;
  1699. steering.x -= boid.velocity.x;
  1700. steering.y -= boid.velocity.y;
  1701. }
  1702. return steering;
  1703. }
  1704. calculateCollision(boid, neighbours,max_force) {
  1705. let steering = { x: 0, y: 0 };
  1706. if (neighbours.length > 0) {
  1707. for (let neighbour of neighbours) {
  1708. if(neighbour == boid) continue
  1709. if(boid.ignore && boid.ignore.includes(neighbour)) continue
  1710. let dx = boid.position.x - neighbour.position.x;
  1711. let dy = boid.position.y - neighbour.position.y;
  1712. // Adjust for wrapping in the x direction
  1713. if (Math.abs(dx) > this.width / 2) {
  1714. dx = dx - Math.sign(dx) * this.width;
  1715. }
  1716. // Adjust for wrapping in the y direction
  1717. if (Math.abs(dy) > this.height / 2) {
  1718. dy = dy - Math.sign(dy) * this.height;
  1719. }
  1720. let difference = { x: dx, y: dy };
  1721. difference = this.normaliseVector(difference);
  1722. steering.x += difference.x;
  1723. steering.y += difference.y;
  1724. }
  1725. if (steering.x !== 0 || steering.y !== 0) {
  1726. steering.x /= neighbours.length;
  1727. steering.y /= neighbours.length;
  1728. steering = this.normaliseVector(steering);
  1729. steering.x *= max_force;
  1730. steering.y *= max_force;
  1731. steering.x -= boid.velocity.x;
  1732. steering.y -= boid.velocity.y;
  1733. }
  1734. }
  1735. boid.overlapping = neighbours.length>1;
  1736. return steering;
  1737. }
  1738. followMouse(boid){
  1739. if(this.mousecoords.x == -1000) return
  1740. let dx = boid.position.x - this.mousecoords.x;
  1741. let dy = boid.position.y - this.mousecoords.y;
  1742. let distance = Math.sqrt(dx*dx + dy*dy);
  1743. if (distance > 0) { // Ensure we don't divide by zero
  1744. boid.velocity.x += (dx / distance) * this.config.mouseattraction * this.max_force * -1;
  1745. boid.velocity.y += (dy / distance) * this.config.mouseattraction * this.max_force * -1;
  1746. }
  1747. }
  1748. steerTowards(boid,x,y,strength){
  1749. let dx = boid.position.x - x;
  1750. let dy = boid.position.y - y;
  1751. let distance = Math.sqrt(dx*dx + dy*dy);
  1752. if (distance > 0) { // Ensure we don't divide by zero
  1753. boid.velocity.x += (dx / distance) * strength * this.max_force * -1;
  1754. boid.velocity.y += (dy / distance) * strength * this.max_force * -1;
  1755. }
  1756. }
  1757. dist(obj1,obj2){
  1758. let dx = obj1.x - obj2.x;
  1759. let dy = obj1.y - obj2.y;
  1760. return(Math.sqrt(dx*dx + dy*dy))
  1761. }
  1762. // Rules like boids, collisions, and gravity are done here
  1763. applyPhysics() {
  1764. for (let i = 0; i < this.boids.length; i++) {
  1765. let boid = this.boids[i];
  1766. let friction = this.friction;
  1767. let gravity = this.config.gravity ?? 0;
  1768. let collision_force = this.config.collision_force ?? 0;
  1769. let max_force = this.max_force;
  1770. let brownian = this.config.brownian ?? 0.0;
  1771. let max_speed = this.max_speed;
  1772. if(boid.locked) continue
  1773. if(boid.max_speed !== undefined) max_speed = boid.max_speed;
  1774. if(boid.friction !== undefined) friction = boid.friction;
  1775. if(boid.max_force !== undefined) max_force = boid.max_force;
  1776. if(boid.gravity !== undefined) gravity = boid.gravity;
  1777. if(boid.collision_force !== undefined) collision_force = boid.collision_force;
  1778. let neighbours = this.getIndividualsInRange(boid.position, this.neighbourhood_radius);
  1779. let alignment = this.config.alignment ? this.calculateAlignment(boid, neighbours,max_speed) : {x:0,y:0};
  1780. let alignmentstrength = this.config.alignment ? this.config.alignment.strength : 0;
  1781. let separation = this.config.separation ? this.calculateSeparation(boid, neighbours,max_speed) : {x:0,y:0};
  1782. let separationstrength = this.config.separation ? this.config.separation.strength : 0;
  1783. let cohesion = this.config.cohesion ? this.calculateCohesion(boid, neighbours,max_speed) : {x:0,y:0};
  1784. let cohesionstrength = this.config.cohesion ? this.config.cohesion.strength : 0;
  1785. if(boid.alignmentstrength !== undefined) alignmentstrength = boid.alignmentstrength;
  1786. if(boid.cohesionstrength !== undefined) cohesionstrength = boid.cohesionstrength;
  1787. if(boid.separationstrength !== undefined) separationstrength = boid.separationstrength;
  1788. if(boid.brownian !== undefined) brownian = boid.brownian;
  1789. let collision = {x:0,y:0};
  1790. if(collision_force > 0){
  1791. let overlapping = this.getIndividualsInRange(boid.position, boid.size);
  1792. collision = this.calculateCollision(boid, overlapping, max_force);
  1793. }
  1794. // Add acceleration to the boid
  1795. boid.acceleration.x += alignment.x * alignmentstrength +
  1796. separation.x * separationstrength +
  1797. cohesion.x * cohesionstrength +
  1798. collision.x * collision_force;
  1799. boid.acceleration.y += alignment.y * alignmentstrength +
  1800. separation.y * separationstrength +
  1801. cohesion.y * cohesionstrength +
  1802. collision.y * collision_force +
  1803. gravity;
  1804. if(this.config.mouseattraction){
  1805. this.followMouse(boid);
  1806. }
  1807. // Limit the force applied to the boid
  1808. let accLength = Math.sqrt(boid.acceleration.x * boid.acceleration.x + boid.acceleration.y * boid.acceleration.y);
  1809. if (accLength > max_force) {
  1810. boid.acceleration.x = (boid.acceleration.x / accLength) * max_force;
  1811. boid.acceleration.y = (boid.acceleration.y / accLength) * max_force;
  1812. }
  1813. // Apply friction (linear, so no drag)
  1814. boid.velocity.x *= (1-friction);
  1815. boid.velocity.y *= (1-friction);
  1816. boid.velocity.x+=brownian*(2*sim.rng.random()-1);
  1817. boid.velocity.y+=brownian*(2*sim.rng.random()-1);
  1818. // Update velocity
  1819. boid.velocity.x += boid.acceleration.x;
  1820. boid.velocity.y += boid.acceleration.y;
  1821. // Limit speed
  1822. let speed = Math.sqrt(boid.velocity.x * boid.velocity.x + boid.velocity.y * boid.velocity.y);
  1823. if (speed > max_speed) {
  1824. boid.velocity.x = (boid.velocity.x / speed) * max_speed;
  1825. boid.velocity.y = (boid.velocity.y / speed) * max_speed;
  1826. }
  1827. speed = Math.sqrt(boid.velocity.x * boid.velocity.x + boid.velocity.y * boid.velocity.y);
  1828. // Update position
  1829. boid.position.x += boid.velocity.x;
  1830. boid.position.y += boid.velocity.y;
  1831. // Check for collision with all obstacles
  1832. for(let obs of this.obstacles){
  1833. this.checkCollisionWithObstacle(boid,obs);
  1834. }
  1835. // Wrap around edges
  1836. if(this.wrap[0]){
  1837. if (boid.position.x < 0) boid.position.x += this.width;
  1838. if (boid.position.x >= this.width) boid.position.x -= this.width;
  1839. }
  1840. else {
  1841. if (boid.position.x < boid.size/2) boid.position.x = boid.size/2, boid.velocity.x *= -this.wrapreflect;
  1842. if (boid.position.x >= this.width - boid.size/2) boid.position.x = this.width - boid.size/2, boid.velocity.x *= -this.wrapreflect;
  1843. }
  1844. if(this.wrap[1]){
  1845. if (boid.position.y < 0) boid.position.y += this.height;
  1846. if (boid.position.y >= this.height) boid.position.y -= this.height;
  1847. }
  1848. else {
  1849. if (boid.position.y < boid.size/2) boid.position.y = boid.size/2, boid.velocity.y *= -this.wrapreflect;
  1850. if (boid.position.y >= this.height - boid.size/2) boid.position.y = this.height - boid.size/2, boid.velocity.y *= -this.wrapreflect;
  1851. }
  1852. // Reset acceleration to 0 each cycle
  1853. boid.acceleration.x = 0;
  1854. boid.acceleration.y = 0;
  1855. }
  1856. }
  1857. inBounds(boid, rect){
  1858. if(!rect) rect = {x:0,y:0,w:this.width,h:this.height};
  1859. let r = boid.size/2;
  1860. return(boid.position.x+r > rect.x && boid.position.y+r > rect.y &&
  1861. boid.position.x-r < rect.x+rect.w && boid.position.y-r < rect.y+rect.h)
  1862. }
  1863. checkCollisionWithObstacle(boid, obs) {
  1864. if(obs.type=="rectangle"){
  1865. // Calculate edges of the ball
  1866. const r = boid.size/2;
  1867. const left = boid.position.x - r;
  1868. const right = boid.position.x + r;
  1869. const top = boid.position.y - r;
  1870. const bottom = boid.position.y + r;
  1871. // Check for collision with rectangle
  1872. if (right > obs.x && left < obs.x + obs.w && bottom > obs.y && top < obs.y + obs.h) {
  1873. const prevX = boid.position.x - boid.velocity.x;
  1874. const prevY = boid.position.y - boid.velocity.y;
  1875. const prevLeft = prevX - r;
  1876. const prevRight = prevX + r;
  1877. const prevTop = prevY - r;
  1878. const prevBottom = prevY + r;
  1879. // Determine where the collision occurred
  1880. if (prevRight <= obs.x || prevLeft >= obs.x + obs.w) {
  1881. boid.velocity.x = -boid.velocity.x*obs.force; // Horizontal collision
  1882. boid.position.x += boid.velocity.x; // Adjust position to prevent sticking
  1883. }
  1884. if (prevBottom <= obs.y || prevTop >= obs.y + obs.h) {
  1885. boid.velocity.y = -boid.velocity.y*obs.force; // Vertical collision
  1886. boid.position.y += boid.velocity.y; // Adjust position to prevent sticking
  1887. }
  1888. }
  1889. }
  1890. else if(obs.type=="circle"){
  1891. let bigr = Math.max(boid.size, obs.r);
  1892. let dx = obs.x - boid.position.x;
  1893. let dy = obs.y - boid.position.y;
  1894. let dist = Math.sqrt(dx*dx + dy*dy);
  1895. if(dist < bigr){
  1896. let difference = { x: dx, y: dy };
  1897. difference = this.normaliseVector(difference);
  1898. boid.velocity.x -= difference.x*obs.force;
  1899. boid.velocity.y -= difference.y*obs.force;
  1900. }
  1901. }
  1902. }
  1903. lengthVector(vector) {
  1904. return(Math.sqrt(vector.x * vector.x + vector.y * vector.y))
  1905. }
  1906. scaleVector(vector,scale){
  1907. return {x:vector.x*scale,y:vector.y*scale}
  1908. }
  1909. normaliseVector(vector) {
  1910. let length = this.lengthVector(vector);
  1911. if (length > 0) return { x: vector.x / length, y: vector.y / length }
  1912. else return { x: 0, y: 0 };
  1913. }
  1914. limitVector = function (vector,length){
  1915. let x = vector.x;
  1916. let y = vector.y;
  1917. let magnitude = Math.sqrt(x*x + y*y);
  1918. if (magnitude > length) {
  1919. // Calculate the scaling factor
  1920. const scalingFactor = length / magnitude;
  1921. // Scale the vector components
  1922. const scaledX = x * scalingFactor;
  1923. const scaledY = y * scalingFactor;
  1924. // Return the scaled vector as an object
  1925. return { x: scaledX, y: scaledY };
  1926. }
  1927. return { x:x, y:y }
  1928. }
  1929. // Angle in degrees
  1930. rotateVector(vec, ang)
  1931. {
  1932. ang = -ang * (Math.PI/180);
  1933. var cos = Math.cos(ang);
  1934. var sin = Math.sin(ang);
  1935. return {x: vec.x*cos - vec.y*sin, y: vec.x*sin + vec.y*cos}
  1936. }
  1937. handleMouseBoids(){}
  1938. repelBoids(force=30){
  1939. for (let boid of this.mouseboids) {
  1940. let dx = boid.position.x - this.mousecoords.x;
  1941. let dy = boid.position.y - this.mousecoords.y;
  1942. let distance = Math.sqrt(dx*dx + dy*dy);
  1943. if (distance > 0) { // Ensure we don't divide by zero
  1944. let strength = (this.mouse_radius - distance) / this.mouse_radius;
  1945. boid.velocity.x += (dx / distance) * strength * this.max_force * force;
  1946. boid.velocity.y += (dy / distance) * strength * this.max_force * force;
  1947. }
  1948. }
  1949. this.mouseboids = [];
  1950. }
  1951. pullBoids(){
  1952. this.repelBoids(-30);
  1953. }
  1954. killBoids(){
  1955. let mouseboids = this.mouseboids;
  1956. this.boids = this.boids.filter( function( el ) {
  1957. return mouseboids.indexOf( el ) < 0;
  1958. } );
  1959. }
  1960. /** Apart from flocking itself, any updates for the individuals are done here.
  1961. * By default, nextState is empty. It should be defined by the user (see examples)
  1962. */
  1963. update() {
  1964. }
  1965. /** If called for the first time, make an update order (list of ints), otherwise just shuffle it. */
  1966. set_update_order() {
  1967. if (typeof this.upd_order === 'undefined') // "Static" variable, only create this array once and reuse it
  1968. {
  1969. this.upd_order = [];
  1970. for (let n = 0; n < this.individuals.length; n++) {
  1971. this.upd_order.push(n);
  1972. }
  1973. }
  1974. shuffle(this.upd_order, this.rng); // Shuffle the update order
  1975. }
  1976. // TODO UITLEG
  1977. getGridpoint(i,gridmodel){
  1978. return gridmodel.grid[Math.floor(i.x)][Math.floor(i.y)]
  1979. }
  1980. // TODO UITLEG
  1981. getNearbyGridpoints(boid,gridmodel,radius){
  1982. let gps = [];
  1983. let ix = Math.floor(boid.position.x);
  1984. let iy = Math.floor(boid.position.y);
  1985. radius = Math.floor(0.5*radius);
  1986. for (let x = ix-radius; x < ix+radius; x++)
  1987. for (let y = iy-radius; y < iy+radius; y++)
  1988. {
  1989. if(!this.wrap[0])
  1990. if(x < 0 || x > this.width-1) continue
  1991. if(!this.wrap[1])
  1992. if(y < 0 || y > this.height-1) continue
  1993. if ((Math.pow((boid.position.x - x), 2) + Math.pow((boid.position.y - y), 2)) < radius*radius){
  1994. gps.push(gridmodel.grid[(x + gridmodel.nc) % gridmodel.nc][(y + gridmodel.nr) % gridmodel.nr]);
  1995. }
  1996. }
  1997. return gps
  1998. }
  1999. getIndividualsInRange(position,radius){
  2000. let qt = this.qt;
  2001. let width = this.width;
  2002. let height = this.height;
  2003. let neighbours = []; // Collect all found neighbours here
  2004. const offsets = [ // Fetch in 9 possible ways for wrapping around the grid
  2005. { x: 0, y: 0 },
  2006. { x: width, y: 0 },
  2007. { x: -width, y: 0 },
  2008. { x: 0, y: height },
  2009. { x: 0, y: -height },
  2010. { x: width, y: height },
  2011. { x: width, y: -height },
  2012. { x: -width, y: height },
  2013. { x: -width, y: -height }
  2014. ];
  2015. // Fetch all neighbours for each range
  2016. for (const offset of offsets) {
  2017. let range = { x:position.x+offset.x, y:position.y+offset.y, w:radius*2, h:radius*2 };
  2018. neighbours.push(...qt.query(range));
  2019. }
  2020. // Filter neighbours to only include those within the circular radius (a bit quicker than slicing in for loop, i noticed)
  2021. return neighbours.filter(neighbour => {
  2022. let dx = neighbour.position.x - position.x;
  2023. let dy = neighbour.position.y - position.y;
  2024. // Adjust for wrapping in the x direction
  2025. if (Math.abs(dx) > width/2) {
  2026. dx = dx - Math.sign(dx) * width;
  2027. }
  2028. // Adjust for wrapping in the y direction
  2029. if (Math.abs(dy) > height/2) {
  2030. dy = dy - Math.sign(dy) * height;
  2031. }
  2032. return (dx*dx + dy*dy) <= (radius*radius);
  2033. });
  2034. }
  2035. /** From a list of individuals, e.g. this.individuals, sample one weighted by a property. This is analogous
  2036. * to spinning a "roulette wheel". Also see a hard-coded versino of this in the "cheater" example
  2037. * @param {Array} individuals Array of individuals to sample from (e.g. living individuals in neighbourhood)
  2038. * @param {string} property The property used to weigh gps (e.g. fitness)
  2039. * @param {float} non Scales the probability of not returning any gp.
  2040. */
  2041. rouletteWheel(individuals, property, non = 0.0) {
  2042. let sum_property = non;
  2043. for (let i = 0; i < individuals.length; i++) sum_property += individuals[i][property]; // Now we have the sum of weight + a constant (non)
  2044. let randomnr = this.rng.genrand_real1() * sum_property; // Sample a randomnr between 0 and sum_property
  2045. let cumsum = 0.0; // This will keep track of the cumulative sum of weights
  2046. for (let i = 0; i < individuals.length; i++) {
  2047. cumsum += individuals[i][property];
  2048. if (randomnr < cumsum) return individuals[i]
  2049. }
  2050. return
  2051. }
  2052. placeObstacle(config){
  2053. let force = config.force == undefined ? 1 : config.force;
  2054. if(config.w) this.obstacles.push({type:'rectangle',x:config.x,y:config.y,w:config.w,h:config.h,fill:config.fill,force:force});
  2055. if(config.r) this.obstacles.push({type:'circle',x:config.x,y:config.y,r:config.r,fill:config.fill,force:force});
  2056. }
  2057. /** Assign each individual a new random position in space. This simulated mixing,
  2058. * but does not guarantee a "well-mixed" system per se (interactions are still local)
  2059. * calculated based on neighbourhoods.
  2060. */
  2061. perfectMix(){
  2062. return "Perfectly mixed the individuals"
  2063. }
  2064. /**
  2065. * Adds a dygraph-plot to your DOM (if the DOM is loaded)
  2066. * @param {Array} graph_labels Array of strings for the graph legend
  2067. * @param {Array} graph_values Array of floats to plot (here plotted over time)
  2068. * @param {Array} cols Array of colours to use for plotting
  2069. * @param {String} title Title of the plot
  2070. * @param {Object} opts dictionary-style list of opts to pass onto dygraphs
  2071. */
  2072. plotArray(graph_labels, graph_values, cols, title, opts) {
  2073. if (typeof window == 'undefined') return
  2074. if (!(title in this.graphs)) {
  2075. cols = parseColours(cols);
  2076. graph_values.unshift(this.time);
  2077. graph_labels.unshift("Time");
  2078. this.graphs[title] = new Graph(graph_labels, graph_values, cols, title, opts);
  2079. }
  2080. else {
  2081. if (this.time % this.graph_interval == 0) {
  2082. graph_values.unshift(this.time);
  2083. graph_labels.unshift("Time");
  2084. this.graphs[title].push_data(graph_values);
  2085. }
  2086. if (this.time % this.graph_update == 0) {
  2087. this.graphs[title].update();
  2088. }
  2089. }
  2090. }
  2091. /**
  2092. * Adds a dygraph-plot to your DOM (if the DOM is loaded)
  2093. * @param {Array} graph_values Array of floats to plot (here plotted over time)
  2094. * @param {String} title Title of the plot
  2095. * @param {Object} opts dictionary-style list of opts to pass onto dygraphs
  2096. */
  2097. plotPoints(graph_values, title, opts) {
  2098. let graph_labels = Array.from({length: graph_values.length}, (v, i) => 'sample'+(i+1));
  2099. let cols = Array.from({length: graph_values.length}, (v, i) => 'black');
  2100. let seriesname = 'average';
  2101. let sum = 0;
  2102. let num = 0;
  2103. // Get average of all defined values
  2104. for(let n = 0; n< graph_values.length; n++){
  2105. if(graph_values[n] !== undefined) {
  2106. sum += graph_values[n];
  2107. num++;
  2108. }
  2109. }
  2110. let avg = (sum / num) || 0;
  2111. graph_values.unshift(avg);
  2112. graph_labels.unshift(seriesname);
  2113. cols.unshift("#666666");
  2114. if(opts == undefined) opts = {};
  2115. opts.drawPoints = true;
  2116. opts.strokeWidth = 0;
  2117. opts.pointSize = 1;
  2118. opts.series = {[seriesname]: {strokeWidth: 3.0, strokeColor:"green", drawPoints: false, pointSize: 0, highlightCircleSize: 3 }};
  2119. if (typeof window == 'undefined') return
  2120. if (!(title in this.graphs)) {
  2121. cols = parseColours(cols);
  2122. graph_values.unshift(this.time);
  2123. graph_labels.unshift("Time");
  2124. this.graphs[title] = new Graph(graph_labels, graph_values, cols, title, opts);
  2125. }
  2126. else {
  2127. if (this.time % this.graph_interval == 0) {
  2128. graph_values.unshift(this.time);
  2129. graph_labels.unshift("Time");
  2130. this.graphs[title].push_data(graph_values);
  2131. }
  2132. if (this.time % this.graph_update == 0) {
  2133. this.graphs[title].update();
  2134. }
  2135. }
  2136. }
  2137. /**
  2138. * Adds a dygraph-plot to your DOM (if the DOM is loaded)
  2139. * @param {Array} graph_labels Array of strings for the graph legend
  2140. * @param {Array} graph_values Array of 2 floats to plot (first value for x-axis, second value for y-axis)
  2141. * @param {Array} cols Array of colours to use for plotting
  2142. * @param {String} title Title of the plot
  2143. * @param {Object} opts dictionary-style list of opts to pass onto dygraphs
  2144. */
  2145. plotXY(graph_labels, graph_values, cols, title, opts) {
  2146. if (typeof window == 'undefined') return
  2147. if (!(title in this.graphs)) {
  2148. cols = parseColours(cols);
  2149. this.graphs[title] = new Graph(graph_labels, graph_values, cols, title, opts);
  2150. }
  2151. else {
  2152. if (this.time % this.graph_interval == 0) {
  2153. this.graphs[title].push_data(graph_values);
  2154. }
  2155. if (this.time % this.graph_update == 0) {
  2156. this.graphs[title].update();
  2157. }
  2158. }
  2159. }
  2160. /**
  2161. * Easy function to add a pop-sizes plot (wrapper for plotArrays)
  2162. * @param {String} property What property to plot (needs to exist in your model, e.g. "species" or "alive")
  2163. * @param {Array} values Which values are plotted (e.g. [1,3,4,6])
  2164. */
  2165. plotPopsizes(property, values, opts) {
  2166. if (typeof window == 'undefined') return
  2167. if (this.time % this.graph_interval != 0 && this.graphs[`Population sizes (${this.name})`] !== undefined) return
  2168. // Wrapper for plotXY function, which expects labels, values, colours, and a title for the plot:
  2169. // Labels
  2170. let graph_labels = [];
  2171. for (let val of values) { graph_labels.push(property + '_' + val); }
  2172. // Values
  2173. let popsizes = this.getPopsizes(property, values);
  2174. let graph_values = popsizes;
  2175. // Colours
  2176. let colours = [];
  2177. for (let c of values) {
  2178. if (this.statecolours[property].constructor != Object)
  2179. colours.push(this.statecolours[property]);
  2180. else
  2181. colours.push(this.statecolours[property][c]);
  2182. }
  2183. // Title
  2184. let title = "Population sizes (" + this.name + ")";
  2185. if(opts && opts.title) title = opts.title;
  2186. this.plotArray(graph_labels, graph_values, colours, title, opts);
  2187. //this.graph = new Graph(graph_labels,graph_values,colours,"Population sizes ("+this.name+")")
  2188. }
  2189. drawSlide(canvasname,prefix="grid_") {
  2190. let canvas = this.canvases[canvasname].elem; // Grab the canvas element
  2191. let timestamp = sim.time.toString();
  2192. timestamp = timestamp.padStart(5, "0");
  2193. canvas.toBlob(function(blob)
  2194. {
  2195. saveAs(blob, prefix+timestamp+".png");
  2196. });
  2197. }
  2198. resetPlots() {
  2199. this.time = 0;
  2200. for (let g in this.graphs) {
  2201. this.graphs[g].reset_plot();
  2202. }
  2203. }
  2204. }
  2205. /**
  2206. * Canvas is a wrapper-class for a HTML-canvas element. It is linked to a @Gridmodel object, and stores what from that @Gridmodel should be displayed (width, height, property, scale, etc.)
  2207. */
  2208. class Canvas {
  2209. /**
  2210. * The constructor function for a @Canvas object.
  2211. * @param {model} model The model ( @Gridmodel or @Flockmodel ) to which this canvas belongs
  2212. * @param {string} property the property that should be shown on the canvas
  2213. * @param {int} height height of the canvas (in rows)
  2214. * @param {int} width width of the canvas (in cols)
  2215. * @param {scale} scale of the canvas (width/height of each gridpoint in pixels)
  2216. */
  2217. constructor(model, prop, lab, height, width, scale, continuous, addToDisplay) {
  2218. this.label = lab;
  2219. this.model = model;
  2220. this.statecolours = model.statecolours;
  2221. this.property = prop;
  2222. this.height = height;
  2223. this.width = width;
  2224. this.scale = scale;
  2225. this.continuous = continuous;
  2226. this.bgcolour = 'black';
  2227. this.offset_x = 0;
  2228. this.offset_y = 0;
  2229. this.phase = 0;
  2230. this.addToDisplay = addToDisplay;
  2231. if (typeof document !== "undefined") // In browser, crease a new HTML canvas-element to draw on
  2232. {
  2233. this.elem = document.createElement("canvas");
  2234. this.titlediv = document.createElement("div");
  2235. if(this.label) this.titlediv.innerHTML = "<p style='height:10'><font size = 3>" + this.label + "</font></p>";
  2236. this.canvasdiv = document.createElement("div");
  2237. this.canvasdiv.className = "grid-holder";
  2238. this.elem.className = "canvas-cacatoo";
  2239. this.elem.width = this.width * this.scale;
  2240. this.elem.height = this.height * this.scale;
  2241. if(!addToDisplay){
  2242. this.canvasdiv.appendChild(this.elem);
  2243. this.canvasdiv.appendChild(this.titlediv);
  2244. document.getElementById("canvas_holder").appendChild(this.canvasdiv);
  2245. }
  2246. this.ctx = this.elem.getContext("2d", { willReadFrequently: true });
  2247. this.display = this.displaygrid;
  2248. }
  2249. this.underlay = function(){};
  2250. this.overlay = function(){};
  2251. }
  2252. /**
  2253. * Draw the state of the model (for a specific property) onto the HTML element
  2254. */
  2255. displaygrid() {
  2256. let ctx = this.ctx;
  2257. let scale = this.scale;
  2258. let ncol = this.width;
  2259. let nrow = this.height;
  2260. let prop = this.property;
  2261. if(this.spacetime){
  2262. ctx.fillStyle = this.bgcolour;
  2263. ctx.fillRect((this.phase%ncol)*scale, 0, scale, nrow * scale);
  2264. }
  2265. else {
  2266. ctx.clearRect(0, 0, scale * ncol, scale * nrow);
  2267. ctx.fillStyle = this.bgcolour;
  2268. ctx.fillRect(0, 0, ncol * scale, nrow * scale);
  2269. }
  2270. this.underlay();
  2271. var id = ctx.getImageData(0, 0, scale * ncol, scale * nrow);
  2272. var pixels = id.data;
  2273. let start_col = this.offset_x;
  2274. let stop_col = start_col + ncol;
  2275. let start_row = this.offset_y;
  2276. let stop_row = start_row + nrow;
  2277. let statecols = this.statecolours[prop];
  2278. for (let x = start_col; x < stop_col; x++) // x are cols
  2279. {
  2280. for (let y = start_row; y< stop_row; y++) // y are rows
  2281. {
  2282. if (!(prop in this.model.grid[x][y]))
  2283. continue
  2284. let value = this.model.grid[x][y][prop];
  2285. if(this.continuous && this.maxval !== undefined && this.minval !== undefined)
  2286. {
  2287. value = Math.max(this.minval,Math.min(this.maxval,value));
  2288. value = Math.ceil((value - this.minval)/(this.maxval-this.minval)*this.num_colours);
  2289. }
  2290. if (statecols[value] == undefined) // Don't draw the background state
  2291. continue
  2292. let idx;
  2293. if (statecols.constructor == Object) {
  2294. idx = statecols[value];
  2295. }
  2296. else idx = statecols;
  2297. for (let n = 0; n < scale; n++) {
  2298. for (let m = 0; m < scale; m++) {
  2299. let xpos = (x-this.offset_x) * scale + n + (this.phase%ncol)*scale;
  2300. let ypos = (y-this.offset_y) * scale + m;
  2301. var off = (ypos * id.width + xpos) * 4;
  2302. pixels[off] = idx[0];
  2303. pixels[off + 1] = idx[1];
  2304. pixels[off + 2] = idx[2];
  2305. }
  2306. }
  2307. }
  2308. if(this.spacetime) {
  2309. this.phase = (this.phase+1);
  2310. break
  2311. }
  2312. }
  2313. ctx.putImageData(id, 0, 0);
  2314. this.overlay();
  2315. }
  2316. /**
  2317. * Draw the state of the model (for a specific property) onto the HTML element
  2318. */
  2319. displaygrid_dots() {
  2320. let ctx = this.ctx;
  2321. let scale = this.scale;
  2322. let ncol = this.width;
  2323. let nrow = this.height;
  2324. let prop = this.property;
  2325. if(this.spacetime){
  2326. ctx.fillStyle = this.bgcolour;
  2327. ctx.fillRect((this.phase%ncol)*scale, 0, scale, nrow * scale);
  2328. }
  2329. else {
  2330. ctx.clearRect(0, 0, scale * ncol, scale * nrow);
  2331. ctx.fillStyle = this.bgcolour;
  2332. ctx.fillRect(0, 0, ncol * scale, nrow * scale);
  2333. }
  2334. this.underlay();
  2335. let start_col = this.offset_x;
  2336. let stop_col = start_col + ncol;
  2337. let start_row = this.offset_y;
  2338. let stop_row = start_row + nrow;
  2339. let statecols = this.statecolours[prop];
  2340. for (let x = start_col; x < stop_col; x++) // x are cols
  2341. {
  2342. for (let y = start_row; y< stop_row; y++) // y are rows
  2343. {
  2344. if (!(prop in this.model.grid[x][y]))
  2345. continue
  2346. let value = this.model.grid[x][y][prop];
  2347. let radius = this.scale_radius*this.radius;
  2348. if(isNaN(radius)) radius = this.scale_radius*this.model.grid[x][y][this.radius];
  2349. if(isNaN(radius)) radius = this.min_radius;
  2350. radius = Math.max(Math.min(radius,this.max_radius),this.min_radius);
  2351. if(this.continuous && value !== 0 && this.maxval !== undefined && this.minval !== undefined)
  2352. {
  2353. value = Math.max(value,this.minval) - this.minval;
  2354. let mult = this.num_colours/(this.maxval-this.minval);
  2355. value = Math.min(this.num_colours,Math.max(Math.floor(value*mult),1));
  2356. }
  2357. if (statecols[value] == undefined) // Don't draw the background state
  2358. continue
  2359. let idx;
  2360. if (statecols.constructor == Object) {
  2361. idx = statecols[value];
  2362. }
  2363. else idx = statecols;
  2364. ctx.beginPath();
  2365. ctx.arc((x-this.offset_x) * scale + 0.5*scale, (y-this.offset_y) * scale + 0.5*scale, radius, 0, 2 * Math.PI, false);
  2366. ctx.fillStyle = 'rgb('+idx[0]+', '+idx[1]+', '+idx[2]+')';
  2367. // ctx.fillStyle = 'rgb(100,100,100)';
  2368. ctx.fill();
  2369. if(this.stroke){
  2370. ctx.lineWidth = this.strokeWidth;
  2371. ctx.strokeStyle = this.strokeStyle;
  2372. ctx.stroke();
  2373. }
  2374. }
  2375. }
  2376. this.overlay();
  2377. }
  2378. /**
  2379. * Draw the state of the flockmodel onto the HTML element
  2380. */
  2381. displayflock() {
  2382. let ctx = this.ctx;
  2383. if(this.model.draw == false) return
  2384. if(this.addToDisplay) this.ctx = this.addToDisplay.ctx;
  2385. let scale = this.scale;
  2386. let ncol = this.width;
  2387. let nrow = this.height;
  2388. let prop = this.property;
  2389. if(!this.addToDisplay) {
  2390. ctx.clearRect(0, 0, scale * ncol, scale * nrow);
  2391. ctx.fillStyle = this.bgcolour;
  2392. ctx.fillRect(0, 0, ncol * scale, nrow * scale);
  2393. }
  2394. this.underlay();
  2395. if(this.model.config.qt_colour) this.model.qt.draw(ctx, this.scale, this.model.config.qt_colour);
  2396. for (let boid of this.model.boids){ // Plot all individuals
  2397. if(boid.invisible) continue
  2398. if(!boid.fill) boid.fill = 'black';
  2399. if(this.model.statecolours[prop]){
  2400. let val = boid[prop];
  2401. if(this.maxval !== undefined){
  2402. let cols = this.model.statecolours[prop];
  2403. val = Math.max(val,this.minval) - this.minval;
  2404. let mult = this.num_colours/(this.maxval-this.minval);
  2405. val = Math.min(this.num_colours,Math.max(Math.floor(val*mult),1));
  2406. boid.fill = rgbToHex(cols[val]);
  2407. }
  2408. else {
  2409. boid.fill = rgbToHex(this.model.statecolours[prop][val]);
  2410. }
  2411. }
  2412. if(boid.col == undefined) boid.col = this.strokeStyle;
  2413. if(boid.lwd == undefined) boid.lwd = this.strokeWidth;
  2414. this.drawBoid(boid,ctx);
  2415. }
  2416. if(this.model.config.draw_mouse_radius){
  2417. ctx.beginPath();
  2418. ctx.strokeStyle = this.model.config.draw_mouse_colour || '#FFFFFF';
  2419. ctx.arc(this.model.mousecoords.x*this.scale, this.model.mousecoords.y*this.scale,this.model.config.mouse_radius*this.scale, 0, Math.PI*2);
  2420. ctx.stroke();
  2421. ctx.closePath();
  2422. }
  2423. for(let obs of this.model.obstacles){
  2424. if(obs.type=='rectangle'){
  2425. ctx.fillStyle = obs.fill || '#00000033';
  2426. ctx.fillRect(obs.x*this.scale, obs.y*this.scale,obs.w*this.scale,obs.h*this.scale);
  2427. }
  2428. else if(obs.type=='circle'){
  2429. ctx.beginPath();
  2430. ctx.fillStyle = obs.fill || '#00000033';
  2431. ctx.lineStyle = '#FFFFFF';
  2432. ctx.arc(obs.x*this.scale,obs.y*this.scale,obs.r*this.scale,0,Math.PI*2);
  2433. ctx.fill();
  2434. ctx.closePath();
  2435. }
  2436. }
  2437. this.draw_qt();
  2438. this.overlay();
  2439. }
  2440. /**
  2441. * This function is empty by default, and is overriden based on parameters chose by the model.
  2442. * Override options are all below this option. Options are:
  2443. * Point: a circle
  2444. * Rect: a square
  2445. * Arrow: an arrow that rotates in the direction the boid is moving
  2446. * Bird: an arrow, but very wide so it looks like a bird
  2447. * Line: a line that has the direction AND length of the velocity vector
  2448. * Ant: three dots form an ant body with two lines forming antanae
  2449. * Png: an image. PNG is sourced from boid.png
  2450. */
  2451. drawBoid(){
  2452. }
  2453. // Draw a circle at the position of the boid
  2454. drawBoidPoint(boid,ctx){
  2455. ctx.fillStyle = boid.fill;
  2456. ctx.beginPath();
  2457. ctx.arc(boid.position.x*this.scale,boid.position.y*this.scale,0.5*boid.size*this.scale,0,Math.PI*2);
  2458. ctx.fill();
  2459. if(boid.col){
  2460. ctx.strokeStyle = boid.col;
  2461. ctx.lineWidth = boid.lwd;
  2462. ctx.stroke();
  2463. }
  2464. ctx.closePath();
  2465. }
  2466. // Draw a rectangle at the position of the boid
  2467. drawBoidRect(boid,ctx){
  2468. ctx.fillStyle = boid.fill;
  2469. ctx.fillRect(boid.position.x*this.scale,boid.position.y*this.scale,boid.size,boid.size);
  2470. if(boid.col){
  2471. ctx.strokeStyle = boid.col;
  2472. ctx.lineWidth = boid.stroke;
  2473. ctx.strokeRect(boid.position.x*this.scale,boid.position.y*this.scale,boid.size,boid.size);
  2474. }
  2475. }
  2476. // Draw an arrow pointing in the direction of the velocity vector
  2477. drawBoidArrow(boid,ctx, length=1, width=0.3){
  2478. ctx.save();
  2479. ctx.translate(boid.position.x*this.scale, boid.position.y*this.scale);
  2480. let angle = Math.atan2(boid.velocity.y*this.scale,boid.velocity.x*this.scale);
  2481. ctx.rotate(angle);
  2482. ctx.fillStyle = boid.fill;
  2483. ctx.beginPath();
  2484. ctx.moveTo(length*boid.size,0);
  2485. ctx.lineTo(0, width*boid.size); // Left wing */
  2486. ctx.lineTo(0, -width*boid.size); // Right wing
  2487. ctx.lineTo(length*boid.size,0); // Back
  2488. ctx.fill();
  2489. if(boid.col){
  2490. ctx.strokeStyle = boid.col;
  2491. ctx.lineWidth = boid.stroke;
  2492. ctx.stroke();
  2493. }
  2494. ctx.restore();
  2495. }
  2496. // Similar to the arrow but very wide. Looks a bit like a bird.
  2497. drawBoidBird(boid,ctx){
  2498. this.drawBoidArrow(boid,ctx,0.4,1);
  2499. }
  2500. // Draw a line from the boids position to the velocity vector. Indicates speed.
  2501. drawBoidLine(boid,ctx){
  2502. ctx.beginPath();
  2503. ctx.strokeStyle = boid.col || boid.fill;
  2504. ctx.lineWidth = boid.stroke;
  2505. ctx.moveTo(boid.position.x*this.scale, boid.position.y*this.scale);
  2506. ctx.lineTo(boid.position.x*this.scale+boid.velocity.x*boid.size,
  2507. boid.position.y*this.scale+boid.velocity.y*boid.size);
  2508. ctx.strokeStyle = boid.fill;
  2509. ctx.stroke();
  2510. ctx.closePath();
  2511. }
  2512. // Draw three points along the velocity vector + 2 antanae. Sort of an ant thingy.
  2513. drawBoidAnt(boid,ctx){
  2514. ctx.fillStyle = boid.fill;
  2515. let vector = this.model.normaliseVector({x: boid.velocity.x, y: boid.velocity.y});
  2516. // First body part
  2517. ctx.beginPath();
  2518. ctx.arc(boid.position.x*this.scale-vector.x*boid.size*1.5,
  2519. boid.position.y*this.scale-vector.y*boid.size*1.5,boid.size*1.2,0,Math.PI*2);
  2520. ctx.fill();
  2521. ctx.closePath();
  2522. // Second body part
  2523. ctx.beginPath();
  2524. ctx.arc(boid.position.x*this.scale,
  2525. boid.position.y*this.scale,
  2526. boid.size/1.3,0,Math.PI*2);
  2527. ctx.fill();
  2528. ctx.closePath();
  2529. // Third body part
  2530. ctx.beginPath();
  2531. ctx.arc(boid.position.x*this.scale+vector.x*boid.size*1.3,
  2532. boid.position.y*this.scale+vector.y*boid.size*1.3,
  2533. boid.size/1.1,0,Math.PI*2);
  2534. ctx.fill();
  2535. ctx.closePath();
  2536. // Food
  2537. if(boid.food){
  2538. ctx.beginPath();
  2539. ctx.fillStyle = boid.food;
  2540. ctx.arc(boid.position.x*this.scale+vector.x*boid.size*3.5,
  2541. boid.position.y*this.scale+vector.y*boid.size*3.5,
  2542. boid.size*1.2,0,Math.PI*2);
  2543. ctx.fill();
  2544. ctx.closePath();
  2545. }
  2546. let dir;
  2547. ctx.beginPath();
  2548. // First antenna
  2549. dir = this.model.rotateVector(vector,30);
  2550. ctx.moveTo(boid.position.x*this.scale+vector.x*boid.size*1,
  2551. boid.position.y*this.scale+vector.y*boid.size*1);
  2552. ctx.lineTo(boid.position.x*this.scale+vector.x*boid.size*1.8+dir.x*boid.size*1.3,
  2553. boid.position.y*this.scale+vector.y*boid.size*1.8+dir.y*boid.size*1.3);
  2554. ctx.strokeStyle = boid.fill;
  2555. ctx.lineWidth = boid.size/2;
  2556. // // Second antenna
  2557. dir = this.model.rotateVector(vector,-30);
  2558. ctx.moveTo(boid.position.x*this.scale+vector.x*boid.size*1,
  2559. boid.position.y*this.scale+vector.y*boid.size*1);
  2560. ctx.lineTo(boid.position.x*this.scale+vector.x*boid.size*1.8+dir.x*boid.size*1.3,
  2561. boid.position.y*this.scale+vector.y*boid.size*1.8+dir.y*boid.size*1.3);
  2562. ctx.strokeStyle = boid.fill;
  2563. ctx.lineWidth = boid.size/2;
  2564. ctx.stroke();
  2565. if(boid.col){
  2566. ctx.strokeStyle = boid.col;
  2567. ctx.lineWidth = boid.stroke;
  2568. ctx.stroke();
  2569. }
  2570. ctx.closePath();
  2571. }
  2572. // Draw an image at the position of the boid. Requires boid.png to be set. Optional is boid.pngangle to
  2573. // let the png adjust direction according to the velocity vector
  2574. drawBoidPng(boid,ctx){
  2575. if(boid.pngangle !==undefined){
  2576. ctx.save();
  2577. ctx.translate(boid.position.x*this.scale-boid.size*this.scale*0.5,
  2578. boid.position.y*this.scale-boid.size*this.scale*0.5);
  2579. let angle = Math.atan2(boid.velocity.y*this.scale,boid.velocity.x*this.scale+boid.pngangle);
  2580. ctx.rotate(angle);
  2581. if(boid.img == undefined) boid.img = new Image();
  2582. boid.img.src = boid.png;
  2583. if(!boid.png) console.warn("Boid does not have a PNG associated with it");
  2584. ctx.drawImage(base_image,0,0,boid.size*this.scale,boid.size*this.scale);
  2585. ctx.restore();
  2586. }
  2587. else {
  2588. if(boid.img == undefined) boid.img = new Image();
  2589. boid.img.src = boid.png;
  2590. if(!boid.png) console.warn("Boid does not have a PNG associated with it");
  2591. ctx.drawImage(boid.img, (boid.position.x-0.5*boid.size)*this.scale,
  2592. (boid.position.y-0.5*boid.size)*this.scale,
  2593. boid.size*this.scale,boid.size*this.scale);
  2594. }
  2595. }
  2596. draw_qt(){
  2597. }
  2598. // Add legend to plot
  2599. add_legend(div,property,lab="")
  2600. {
  2601. if (typeof document == "undefined") return
  2602. let statecols = this.statecolours[property];
  2603. if(statecols == undefined){
  2604. console.warn(`Cacatoo warning: no colours setup for canvas "${this.label}"`);
  2605. return
  2606. }
  2607. this.legend = document.createElement("canvas");
  2608. this.legend.className = "legend";
  2609. this.legend.width = this.width*this.scale*0.6;
  2610. this.legend.height = 50;
  2611. let ctx = this.legend.getContext("2d");
  2612. ctx.textAlign = "center";
  2613. ctx.font = '14px helvetica';
  2614. ctx.fillText(lab, this.legend.width/2, 16);
  2615. if(this.maxval!==undefined) {
  2616. let bar_width = this.width*this.scale*0.48;
  2617. let offset = 0.1*this.legend.width;
  2618. let n_ticks = this.nticks-1;
  2619. let tick_increment = (this.maxval-this.minval) / n_ticks;
  2620. let step_size = (this.legend.width / n_ticks)*0.8;
  2621. for(let i=0;i<bar_width;i++)
  2622. {
  2623. let colval = Math.ceil(this.num_colours*i/bar_width);
  2624. if(statecols[colval] == undefined) {
  2625. ctx.fillStyle = this.bgcolor;
  2626. }
  2627. else {
  2628. ctx.fillStyle = rgbToHex(statecols[colval]);
  2629. }
  2630. ctx.fillRect(offset+i, 20, 1, 10);
  2631. ctx.closePath();
  2632. }
  2633. for(let i = 0; i<n_ticks+1; i++){
  2634. let tick_position = (i*step_size+offset);
  2635. ctx.strokeStyle = "#FFFFFF";
  2636. ctx.beginPath();
  2637. ctx.moveTo(tick_position, 25);
  2638. ctx.lineTo(tick_position, 30);
  2639. ctx.lineWidth=2;
  2640. ctx.stroke();
  2641. ctx.closePath();
  2642. ctx.fillStyle = "#000000";
  2643. ctx.textAlign = "center";
  2644. ctx.font = '12px helvetica';
  2645. let ticklab = (this.minval+i*tick_increment);
  2646. ticklab = ticklab.toFixed(this.decimals);
  2647. ctx.fillText(ticklab, tick_position, 45);
  2648. }
  2649. ctx.beginPath();
  2650. ctx.rect(offset, 20, bar_width, 10);
  2651. ctx.strokeStyle = "#000000";
  2652. ctx.stroke();
  2653. ctx.closePath();
  2654. div.appendChild(this.legend);
  2655. }
  2656. else {
  2657. let keys = Object.keys(statecols);
  2658. let total_num_values = keys.length;
  2659. let spacing = 0.9;
  2660. // if(total_num_values < 8) spacing = 0.7
  2661. // if(total_num_values < 4) spacing = 0.8
  2662. let bar_width = this.width*this.scale*spacing;
  2663. let step_size = Math.round(bar_width / (total_num_values+1));
  2664. let offset = this.legend.width*0.5 - step_size*(total_num_values-1)/2;
  2665. for(let i=0;i<total_num_values;i++)
  2666. {
  2667. let pos = offset+Math.floor(i*step_size);
  2668. ctx.beginPath();
  2669. ctx.strokeStyle = "#000000";
  2670. if(statecols[keys[i]] == undefined) ctx.fillStyle = this.bgcolor;
  2671. else ctx.fillStyle = rgbToHex(statecols[keys[i]]);
  2672. if(this.radius){
  2673. ctx.beginPath();
  2674. ctx.arc(pos,10,5,0,Math.PI*2);
  2675. ctx.fill();
  2676. ctx.closePath();
  2677. }
  2678. else {
  2679. ctx.fillRect(pos-4, 10, 10, 10);
  2680. }
  2681. ctx.closePath();
  2682. ctx.font = '12px helvetica';
  2683. ctx.fillStyle = "#000000";
  2684. ctx.textAlign = "center";
  2685. ctx.fillText(keys[i], pos, 35);
  2686. }
  2687. div.appendChild(this.legend);
  2688. }
  2689. }
  2690. remove_legend()
  2691. {
  2692. this.legend.getContext("2d").clearRect(0, 0, this.legend.width, this.legend.height);
  2693. }
  2694. }
  2695. /*
  2696. Functions below are to make sure dygraphs understands the colours used by Cacatoo (converts to hex)
  2697. */
  2698. function componentToHex(c) {
  2699. var hex = c.toString(16);
  2700. return hex.length == 1 ? "0" + hex : hex;
  2701. }
  2702. function rgbToHex(arr) {
  2703. if(arr.length==3) return "#" + componentToHex(arr[0]) + componentToHex(arr[1]) + componentToHex(arr[2])
  2704. if(arr.length==4) return "#" + componentToHex(arr[0]) + componentToHex(arr[1]) + componentToHex(arr[2]) + componentToHex(arr[3])
  2705. }
  2706. // Browser-friendly version of 'fast-random' (https://github.com/borilla/fast-random) by Bram van Dijk for compatibility with browser-based Cacatoo models
  2707. if (typeof module !== 'undefined') module.exports = random;
  2708. function random(seed) {
  2709. function _seed(s) {
  2710. if ((seed = (s|0) % 2147483647) <= 0) {
  2711. seed += 2147483646;
  2712. }
  2713. }
  2714. function _nextInt() {
  2715. return seed = seed * 48271 % 2147483647;
  2716. }
  2717. function _nextFloat() {
  2718. return (_nextInt() - 1) / 2147483646;
  2719. }
  2720. _seed(seed);
  2721. return {
  2722. seed: _seed,
  2723. nextInt: _nextInt,
  2724. nextFloat: _nextFloat
  2725. };
  2726. }
  2727. //module.exports = random;
  2728. /**
  2729. * Simulation is the global class of Cacatoo, containing the main configuration
  2730. * for making a grid-based model and displaying it in either browser or with
  2731. * nodejs.
  2732. */
  2733. class Simulation {
  2734. /**
  2735. * The constructor function for a @Simulation object. Takes a config dictionary.
  2736. * and sets options accordingly.
  2737. * @param {dictionary} config A dictionary (object) with all the necessary settings to setup a Cacatoo simulation.
  2738. */
  2739. constructor(config) {
  2740. if(config == undefined) config = {};
  2741. this.config = config;
  2742. this.rng = this.setupRandom(config.seed);
  2743. // this.rng_old = new MersenneTwister(config.seed || 53);
  2744. this.sleep = config.sleep = config.sleep || 0;
  2745. this.maxtime = config.maxtime = config.maxtime || 1000000;
  2746. this.ncol = config.ncol = config.ncol || 100;
  2747. this.nrow = config.nrow = config.nrow || 100;
  2748. this.scale = config.scale = config.scale || 2;
  2749. this.skip = config.skip || 0;
  2750. this.graph_interval = config.graph_interval = config.graph_interval || 10;
  2751. this.graph_update = config.graph_update= config.graph_update || 50;
  2752. this.fps = config.fps * 1.4 || 60; // Multiplied by 1.4 to adjust for overhead
  2753. this.mousecoords = {x:-1000, y:-1000};
  2754. // Three arrays for all the grids ('CAs'), canvases ('displays'), and graphs
  2755. this.gridmodels = []; // All gridmodels in this simulation
  2756. this.flockmodels = []; // All gridmodels in this simulation
  2757. this.canvases = []; // Array with refs to all canvases (from all models) from this simulation
  2758. this.graphs = []; // All graphs
  2759. this.time = 0;
  2760. this.inbrowser = (typeof document !== "undefined");
  2761. this.fpsmeter = false;
  2762. if(config.fpsmeter == true) this.fpsmeter = true;
  2763. this.printcursor = true;
  2764. if(config.printcursor == false) this.printcursor = false;
  2765. }
  2766. /**
  2767. * Generate a new GridModel within this simulation.
  2768. * @param {string} name The name of your new model, e.g. "gol" for game of life. Cannot contain whitespaces.
  2769. */
  2770. makeGridmodel(name) {
  2771. if (name.indexOf(' ') >= 0) throw new Error("The name of a gridmodel cannot contain whitespaces.")
  2772. let model = new Gridmodel(name, this.config, this.rng); // ,this.config.show_gridname weggecomment
  2773. this[name] = model; // this = model["cheater"] = CA-obj
  2774. this.gridmodels.push(model);
  2775. }
  2776. /**
  2777. * Generate a new GridModel within this simulation.
  2778. * @param {string} name The name of your new model, e.g. "gol" for game of life. Cannot contain whitespaces.
  2779. */
  2780. makeFlockmodel(name, cfg) {
  2781. let cfg_combined = {...this.config,...cfg};
  2782. if (name.indexOf(' ') >= 0) throw new Error("The name of a gridmodel cannot contain whitespaces.")
  2783. let model = new Flockmodel(name, cfg_combined, this.rng); // ,this.config.show_gridname weggecomment
  2784. this[name] = model; // this = model["cheater"] = CA-obj
  2785. this.flockmodels.push(model);
  2786. }
  2787. /**
  2788. * Set up the random number generator
  2789. * @param {int} seed Seed for fast-random module
  2790. */
  2791. setupRandom(seed){
  2792. // Load mersennetwister random number generator
  2793. // genrand_real1() [0,1]
  2794. // genrand_real2() [0,1)
  2795. // genrand_real3() (0,1)
  2796. // genrand_int(min,max) integer between min and max
  2797. // let rng = new MersenneTwister(seed || 53) // Use this if you need a more precise RNG
  2798. let rng = random(seed);
  2799. rng.genrand_real1 = function () { return (rng.nextInt() - 1) / 2147483645 }; // Generate random number in [0,1] range
  2800. rng.genrand_real2 = function () { return (rng.nextInt() - 1) / 2147483646 }; // Generate random number in [0,1) range
  2801. rng.genrand_real3 = function () { return rng.nextInt() / 2147483647 }; // Generate random number in (0,1) range
  2802. rng.genrand_int = function (min,max) { return min+ rng.nextInt() % (max-min+1) }; // Generate random integer between (and including) min and max
  2803. for(let i = 0; i < 1000; i++) rng.genrand_real2();
  2804. rng.random = () => { return rng.genrand_real2() };
  2805. rng.randomInt = () => { return rng.genrand_int() };
  2806. rng.randomGaus = (mean=0, stdev=1) => { // Standard gaussian sample
  2807. const u = 1 - sim.rng.random();
  2808. const v = sim.rng.random();
  2809. const z = Math.sqrt( -2.0 * Math.log( u ) ) * Math.cos( 2.0 * Math.PI * v );
  2810. return z * stdev + mean;
  2811. };
  2812. return rng
  2813. }
  2814. /**
  2815. * Create a display for a gridmodel, showing a certain property on it.
  2816. * @param {string} name The name of an existing gridmodel to display
  2817. * @param {string} property The name of the property to display
  2818. * @param {string} customlab Overwrite the display name with something more descriptive
  2819. * @param {integer} height Number of rows to display (default = ALL)
  2820. * @param {integer} width Number of cols to display (default = ALL)
  2821. * @param {integer} scale Scale of display (default inherited from @Simulation class)
  2822. */
  2823. createDisplay(name, property, customlab, height, width, scale) {
  2824. if(! this.inbrowser) {
  2825. console.warn("Cacatoo:createDisplay, cannot create display in command-line mode.");
  2826. return
  2827. }
  2828. if(typeof arguments[0] === 'object')
  2829. {
  2830. name = arguments[0].name;
  2831. property = arguments[0].property;
  2832. customlab = arguments[0].label;
  2833. height = arguments[0].height;
  2834. width = arguments[0].width;
  2835. scale = arguments[0].scale;
  2836. }
  2837. if(name==undefined || property == undefined) throw new Error("Cacatoo: can't make a display with out a 'name' and 'property'")
  2838. let label = customlab;
  2839. if (customlab == undefined) label = `${name} (${property})`; // <ID>_NAME_(PROPERTY)
  2840. let gridmodel = this[name];
  2841. if (gridmodel == undefined) throw new Error(`There is no GridModel with the name ${name}`)
  2842. if (height == undefined) height = gridmodel.nr;
  2843. if (width == undefined) width = gridmodel.nc;
  2844. if (scale == undefined) scale = gridmodel.scale;
  2845. if(gridmodel.statecolours[property]==undefined){
  2846. console.log(`Cacatoo: no fill colour supplied for property ${property}. Using default and hoping for the best.`);
  2847. gridmodel.statecolours[property] = default_colours(10);
  2848. }
  2849. let cnv = new Canvas(gridmodel, property, label, height, width, scale);
  2850. gridmodel.canvases[label] = cnv; // Add a reference to the canvas to the gridmodel
  2851. this.canvases.push(cnv); // Add a reference to the canvas to the sim
  2852. const canvas = cnv;
  2853. cnv.add_legend(cnv.canvasdiv,property);
  2854. cnv.bgcolour = this.config.bgcolour || 'black';
  2855. canvas.elem.addEventListener('mousedown', (e) => { this.printCursorPosition(canvas, e, scale); }, false);
  2856. canvas.elem.addEventListener('mousedown', (e) => { this.active_canvas = canvas; }, false);
  2857. cnv.displaygrid();
  2858. }
  2859. createGridDisplay = this.createDisplay
  2860. /**
  2861. * Create a display for a flockmodel, showing the boids
  2862. * @param {string} name The name of an existing gridmodel to display
  2863. * @param {Object} config All the options for the flocking behaviour etc.
  2864. */
  2865. createFlockDisplay(name, config = {}) {
  2866. if(! this.inbrowser) {
  2867. console.warn("Cacatoo:createFlockDisplay, cannot create display in command-line mode.");
  2868. return
  2869. }
  2870. if(name==undefined) throw new Error("Cacatoo: can't make a display with out a 'name'")
  2871. let flockmodel = this[name];
  2872. if (flockmodel == undefined) throw new Error(`There is no GridModel with the name ${name}`)
  2873. let property = config.property;
  2874. let addToDisplay = config.addToDisplay;
  2875. let label = config.label || "";
  2876. let legendlabel = config.legendlabel;
  2877. let height = sim.height || flockmodel.height;
  2878. let width = config.width || sim.width || flockmodel.width;
  2879. let scale = config.scale || this[name].scale;
  2880. let maxval = config.maxval || this.maxval || undefined;
  2881. let decimals= config.decimals || 0;
  2882. let nticks= config.nticks || 5;
  2883. let minval = config.minval || 0;
  2884. let num_colours = config.num_colours || 100;
  2885. if(config.fill == "viridis") this[name].colourViridis(property, num_colours);
  2886. else if(config.fill == "inferno") this[name].colourViridis(property, num_colours, false, "inferno");
  2887. else if(config.fill == "inferno_rev") this[name].colourViridis(property, num_colours, true, "inferno");
  2888. else if(config.fill == "red") this[name].colourGradient(property, num_colours, [0, 0, 0], [255, 0, 0]);
  2889. else if(config.fill == "green") this[name].colourGradient(property, num_colours, [0, 0, 0], [0, 255, 0]);
  2890. else if(config.fill == "blue") this[name].colourGradient(property, num_colours, [0, 0, 0], [0, 0, 255]);
  2891. else if(this[name].statecolours[property]==undefined && property){
  2892. console.log(`Cacatoo: no fill colour supplied for property ${property}. Using default and hoping for the best.`);
  2893. this[name].colourGradient(property, num_colours, [0, 0, 0], [0, 0, 255]);
  2894. }
  2895. let cnv = new Canvas(flockmodel, property, label, height, width,scale,false,addToDisplay);
  2896. if (maxval !== undefined) cnv.maxval = maxval;
  2897. if (minval !== undefined) cnv.minval = minval;
  2898. if (num_colours !== undefined) cnv.num_colours = num_colours;
  2899. if (decimals !== undefined) cnv.decimals = decimals;
  2900. if (nticks !== undefined) cnv.nticks = nticks;
  2901. cnv.strokeStyle = config.strokeStyle;
  2902. cnv.strokeWidth = config.strokeWidth;
  2903. if(config.legend!==false){
  2904. if(addToDisplay) cnv.add_legend(addToDisplay.canvasdiv,property,legendlabel);
  2905. else cnv.add_legend(cnv.canvasdiv,property,legendlabel );
  2906. }
  2907. // Set the shape of the boid
  2908. let shape = flockmodel.config.shape || 'dot';
  2909. cnv.drawBoid = cnv.drawBoidArrow;
  2910. if(shape == 'bird') cnv.drawBoid = cnv.drawBoidBird;
  2911. else if(shape == 'arrow') cnv.drawBoid = cnv.drawBoidArrow;
  2912. else if(shape == 'rect') cnv.drawBoid = cnv.drawBoidRect;
  2913. else if(shape == 'dot') cnv.drawBoid = cnv.drawBoidPoint;
  2914. else if(shape == 'ant') cnv.drawBoid = cnv.drawBoidAnt;
  2915. else if(shape == 'line') cnv.drawBoid = cnv.drawBoidLine;
  2916. else if(shape == 'png') cnv.drawBoid = cnv.drawBoidPng;
  2917. // Replace function that handles the left mousebutton
  2918. let click = flockmodel.config.click || 'none';
  2919. if(click == 'repel') flockmodel.handleMouseBoids = flockmodel.repelBoids;
  2920. else if(click == 'pull') flockmodel.handleMouseBoids = flockmodel.pullBoids;
  2921. else if(click == 'kill') flockmodel.handleMouseBoids = flockmodel.killBoids;
  2922. flockmodel.canvases[label] = cnv; // Add a reference to the canvas to the flockmodel
  2923. this.canvases.push(cnv); // Add a reference to the canvas to the sim
  2924. const canvas = addToDisplay || cnv;
  2925. flockmodel.mouseDown = false;
  2926. flockmodel.mouseClick = false;
  2927. canvas.elem.addEventListener('mousemove', (e) => {
  2928. let mouse = this.getCursorPosition(canvas,e,1,false);
  2929. if(mouse.x == this.mousecoords.x && mouse.y == this.mousecoords.y) return this.mousecoords
  2930. this.mousecoords = {x:mouse.x/this.scale, y:mouse.y/this.scale};
  2931. flockmodel.mousecoords = this.mousecoords;
  2932. });
  2933. canvas.elem.addEventListener('touchmove', (e) => {
  2934. let mouse = this.getCursorPosition(canvas,e.touches[0],1,false);
  2935. if(mouse.x == this.mousecoords.x && mouse.y == this.mousecoords.y) return this.mousecoords
  2936. this.mousecoords = {x:mouse.x/this.scale, y:mouse.y/this.scale};
  2937. flockmodel.mousecoords = this.mousecoords;
  2938. e.preventDefault();
  2939. });
  2940. canvas.elem.addEventListener('mousedown', (e) => { flockmodel.mouseDown = true;});
  2941. canvas.elem.addEventListener('touchstart', (e) => { flockmodel.mouseDown = true;});
  2942. canvas.elem.addEventListener('mouseup', (e) => { flockmodel.mouseDown = false; });
  2943. canvas.elem.addEventListener('touchend', (e) => { flockmodel.mouseDown = false; flockmodel.mousecoords = {x:-1000,y:-1000};});
  2944. canvas.elem.addEventListener('mouseout', (e) => { flockmodel.mousecoords = {x:-1000,y:-1000};});
  2945. cnv.bgcolour = this.config.bgcolour || 'black';
  2946. cnv.display = cnv.displayflock;
  2947. cnv.display();
  2948. }
  2949. /**
  2950. * Create a display for a gridmodel, showing a certain property on it.
  2951. * @param {object} config Object with the keys name, property, label, width, height, scale, minval, maxval, nticks, decimals, num_colours, fill
  2952. * These keys:value pairs are:
  2953. * @param {string} name The name of the model to display
  2954. * @param {string} property The name of the property to display
  2955. * @param {string} customlab Overwrite the display name with something more descriptive
  2956. * @param {integer} height Number of rows to display (default = ALL)
  2957. * @param {integer} width Number of cols to display (default = ALL)
  2958. * @param {integer} scale Scale of display (default inherited from @Simulation class)
  2959. * @param {numeric} minval colour scale is capped off below this value
  2960. * @param {numeric} maxval colour scale is capped off above this value
  2961. * @param {integer} nticks how many ticks
  2962. * @param {integer} decimals how many decimals for tick labels
  2963. * @param {integer} num_colours how many steps in the colour gradient
  2964. * @param {string} fill type of gradient to use (viridis, inferno, red, green, blue)
  2965. */
  2966. createDisplay_discrete(config) {
  2967. if(! this.inbrowser) {
  2968. console.warn("Cacatoo:createDisplay_discrete, cannot create display in command-line mode.");
  2969. return
  2970. }
  2971. let name = config.model;
  2972. let property = config.property;
  2973. let legend = true;
  2974. if(config.legend == false) legend = false;
  2975. let label = config.label;
  2976. if (label == undefined) label = `${name} (${property})`; // <ID>_NAME_(PROPERTY)
  2977. let gridmodel = this[name];
  2978. if (gridmodel == undefined) throw new Error(`There is no GridModel with the name ${name}`)
  2979. let height = config.height || this[name].nr;
  2980. let width = config.width || this[name].nc;
  2981. let scale = config.scale || this[name].scale;
  2982. let legendlabel = config.legendlabel;
  2983. if(name==undefined || property == undefined) throw new Error("Cacatoo: can't make a display with out a 'name' and 'property'")
  2984. if (gridmodel == undefined) throw new Error(`There is no GridModel with the name ${name}`)
  2985. if (height == undefined) height = gridmodel.nr;
  2986. if (width == undefined) width = gridmodel.nc;
  2987. if (scale == undefined) scale = gridmodel.scale;
  2988. if(gridmodel.statecolours[property]==undefined){
  2989. console.log(`Cacatoo: no fill colour supplied for property ${property}. Using default and hoping for the best.`);
  2990. gridmodel.statecolours[property] = default_colours(10);
  2991. }
  2992. let cnv = new Canvas(gridmodel, property, label, height, width, scale);
  2993. if(config.drawdots) {
  2994. cnv.display = cnv.displaygrid_dots;
  2995. cnv.stroke = config.stroke;
  2996. cnv.strokeStyle = config.strokeStyle;
  2997. cnv.strokeWidth = config.strokeWidth;
  2998. cnv.radius = config.radius || 10;
  2999. cnv.max_radius = config.max_radius || 10;
  3000. cnv.scale_radius = config.scale_radius || 1;
  3001. cnv.min_radius = config.min_radius || 0;
  3002. }
  3003. gridmodel.canvases[label] = cnv; // Add a reference to the canvas to the gridmodel
  3004. this.canvases.push(cnv); // Add a reference to the canvas to the sim
  3005. const canvas = cnv;
  3006. if(legend) cnv.add_legend(cnv.canvasdiv,property, legendlabel);
  3007. cnv.bgcolour = this.config.bgcolour || 'black';
  3008. canvas.elem.addEventListener('mousedown', (e) => { this.printCursorPosition(canvas, e, scale); }, false);
  3009. canvas.elem.addEventListener('mousedown', (e) => { this.active_canvas = canvas; }, false);
  3010. cnv.displaygrid();
  3011. }
  3012. /**
  3013. * Create a display for a gridmodel, showing a certain property on it.
  3014. * @param {object} config Object with the keys name, property, label, width, height, scale, minval, maxval, nticks, decimals, num_colours, fill
  3015. * These keys:value pairs are:
  3016. * @param {string} name The name of the model to display
  3017. * @param {string} property The name of the property to display
  3018. * @param {string} customlab Overwrite the display name with something more descriptive
  3019. * @param {integer} height Number of rows to display (default = ALL)
  3020. * @param {integer} width Number of cols to display (default = ALL)
  3021. * @param {integer} scale Scale of display (default inherited from @Simulation class)
  3022. * @param {numeric} minval colour scale is capped off below this value
  3023. * @param {numeric} maxval colour scale is capped off above this value
  3024. * @param {integer} nticks how many ticks
  3025. * @param {integer} decimals how many decimals for tick labels
  3026. * @param {integer} num_colours how many steps in the colour gradient
  3027. * @param {string} fill type of gradient to use (viridis, inferno, red, green, blue)
  3028. */
  3029. createDisplay_continuous(config) {
  3030. if(! this.inbrowser) {
  3031. console.warn("Cacatoo:createDisplay_continuous, cannot create display in command-line mode.");
  3032. return
  3033. }
  3034. let name = config.model;
  3035. let property = config.property;
  3036. let label = config.label;
  3037. let legendlabel = config.legendlabel;
  3038. let legend = true;
  3039. if(config.legend == false) legend = false;
  3040. if (label == undefined) label = `${name} (${property})`; // <ID>_NAME_(PROPERTY)
  3041. let gridmodel = this[name];
  3042. if (gridmodel == undefined) throw new Error(`There is no GridModel with the name ${name}`)
  3043. let height = config.height || this[name].nr;
  3044. let width = config.width || this[name].nc;
  3045. let scale = config.scale || this[name].scale;
  3046. let maxval = config.maxval || this.maxval || undefined;
  3047. let decimals= config.decimals || 0;
  3048. let nticks= config.nticks || 5;
  3049. let minval = config.minval || 0;
  3050. let num_colours = config.num_colours || 100;
  3051. if(config.fill == "viridis") this[name].colourViridis(property, num_colours);
  3052. else if(config.fill == "inferno") this[name].colourViridis(property, num_colours, false, "inferno");
  3053. else if(config.fill == "inferno_rev") this[name].colourViridis(property, num_colours, true, "inferno");
  3054. else if(config.fill == "red") this[name].colourGradient(property, num_colours, [0, 0, 0], [255, 0, 0]);
  3055. else if(config.fill == "green") this[name].colourGradient(property, num_colours, [0, 0, 0], [0, 255, 0]);
  3056. else if(config.fill == "blue") this[name].colourGradient(property, num_colours, [0, 0, 0], [0, 0, 255]);
  3057. else if(this[name].statecolours[property]==undefined){
  3058. console.log(`Cacatoo: no fill colour supplied for property ${property}. Using default and hoping for the best.`);
  3059. this[name].colourGradient(property, num_colours, [0, 0, 0], [0, 0, 255]);
  3060. }
  3061. let cnv = new Canvas(gridmodel, property, label, height, width, scale, true);
  3062. if(config.drawdots) {
  3063. cnv.display = cnv.displaygrid_dots;
  3064. cnv.stroke = config.stroke;
  3065. cnv.strokeStyle = config.strokeStyle;
  3066. cnv.strokeWidth = config.strokeWidth;
  3067. cnv.radius = config.radius || 10;
  3068. cnv.max_radius = config.max_radius || 10;
  3069. cnv.scale_radius = config.scale_radius || 1;
  3070. cnv.min_radius = config.min_radius || 0;
  3071. }
  3072. gridmodel.canvases[label] = cnv; // Add a reference to the canvas to the gridmodel
  3073. if (maxval !== undefined) cnv.maxval = maxval;
  3074. if (minval !== undefined) cnv.minval = minval;
  3075. if (num_colours !== undefined) cnv.num_colours = num_colours;
  3076. if (decimals !== undefined) cnv.decimals = decimals;
  3077. if (nticks !== undefined) cnv.nticks = nticks;
  3078. if(legend!==false) cnv.add_legend(cnv.canvasdiv,property,legendlabel);
  3079. cnv.bgcolour = this.config.bgcolour || 'black';
  3080. this.canvases.push(cnv); // Add a reference to the canvas to the sim
  3081. const canvas = cnv;
  3082. canvas.elem.addEventListener('mousedown', (e) => { this.printCursorPosition(cnv, e, scale); }, false);
  3083. canvas.elem.addEventListener('mousedown', (e) => { this.active_canvas = canvas; }, false);
  3084. cnv.displaygrid();
  3085. }
  3086. /**
  3087. * Create a space time display for a gridmodel
  3088. * @param {string} name The name of an existing gridmodel to display
  3089. * @param {string} source_canvas_label The name of the property to display
  3090. * @param {string} label Overwrite the display name with something more descriptive
  3091. * @param {integer} col_to_draw Col to display (default = center)
  3092. * @param {integer} ncol Number of cols (i.e. time points) to display (default = ncol)
  3093. * @param {integer} scale Scale of display (default inherited from @Simulation class)
  3094. */
  3095. spaceTimePlot(name, source_canvas_label, label, col_to_draw, ncolumn) {
  3096. if(! this.inbrowser) {
  3097. console.warn("Cacatoo:spaceTimePlot, cannot create display in command-line mode.");
  3098. return
  3099. }
  3100. let source_canvas = this[name].canvases[source_canvas_label];
  3101. let property = source_canvas.property;
  3102. let height = source_canvas.height;
  3103. let width = ncolumn;
  3104. let scale = source_canvas.scale;
  3105. let cnv = new Canvas(this[name], property, label, height, width, scale);
  3106. cnv.spacetime=true;
  3107. cnv.offset_x = col_to_draw;
  3108. cnv.continuous = source_canvas.continuous;
  3109. cnv.minval = source_canvas.minval;
  3110. cnv.maxval = source_canvas.maxval;
  3111. cnv.num_colours = source_canvas.num_colours;
  3112. cnv.ctx.fillRect(0, 0, width*scale , height*scale);
  3113. this[name].canvases[label] = cnv; // Add a reference to the canvas to the gridmodel
  3114. this.canvases.push(cnv); // Add a reference to the canvas to the sim
  3115. var newCanvas = document.createElement('canvas');
  3116. var context = newCanvas.getContext('2d');
  3117. newCanvas.width = source_canvas.legend.width;
  3118. newCanvas.height = source_canvas.legend.height;
  3119. context.drawImage(source_canvas.legend, 0, 0);
  3120. cnv.canvasdiv.appendChild(newCanvas);
  3121. cnv.bgcolour = this.config.bgcolour || 'black';
  3122. cnv.elem.addEventListener('mousedown', (e) => { sim.active_canvas = cnv; }, false);
  3123. }
  3124. /**
  3125. * Get the position of the cursor on the canvas
  3126. * @param {canvas} canvas A (constant) canvas object
  3127. * @param {event-handler} event Event handler (mousedown)
  3128. * @param {scale} scale The zoom (scale) of the grid to grab the correct grid point
  3129. */
  3130. getCursorPosition(canvas, event, scale, floor=true) {
  3131. const rect = canvas.elem.getBoundingClientRect();
  3132. let x = Math.max(0, event.clientX - rect.left) / scale + canvas.offset_x;
  3133. let y = Math.max(0, event.clientY - rect.top) / scale + canvas.offset_y;
  3134. if(floor){
  3135. x = Math.floor(x);
  3136. y = Math.floor(y);
  3137. }
  3138. return({x:x,y:y})
  3139. }
  3140. /**
  3141. * Get *and print the GP* at the cursor position
  3142. * @param {canvas} canvas A (constant) canvas object
  3143. * @param {event-handler} event Event handler (mousedown)
  3144. * @param {scale} scale The zoom (scale) of the grid to grab the correct grid point
  3145. */
  3146. printCursorPosition(canvas, event, scale){
  3147. if(!this.printcursor) return
  3148. let coords = this.getCursorPosition(canvas,event,scale);
  3149. let x = coords.x;
  3150. let y = coords.y;
  3151. if( x< 0 || x >= this.ncol || y < 0 || y >= this.nrow) return
  3152. console.log(`You have clicked the grid at position ${x},${y}, which has:`);
  3153. for (let model of this.gridmodels) {
  3154. console.log("grid point:", model.grid[x][y]);
  3155. }
  3156. for (let model of this.flockmodels) {
  3157. console.log("boids:", model.mouseboids);
  3158. }
  3159. }
  3160. /**
  3161. * Update all the grid models one step. Apply optional mixing
  3162. */
  3163. step() {
  3164. for (let i = 0; i < this.gridmodels.length; i++){
  3165. this.gridmodels[i].update();
  3166. this.gridmodels[i].time++;
  3167. }
  3168. for (let i = 0; i < this.flockmodels.length; i++){
  3169. let model = this.flockmodels[i];
  3170. model.flock();
  3171. model.update();
  3172. model.time++;
  3173. let mouse = model.mousecoords;
  3174. if(model.mouse_radius) model.mouseboids = model.getIndividualsInRange(mouse,model.mouse_radius);
  3175. if(model.mouseDown)model.handleMouseBoids();
  3176. }
  3177. for (let i = 0; i < this.canvases.length; i++)
  3178. if(this.canvases[i].recording == true)
  3179. this.captureFrame(this.canvases[i]);
  3180. this.time++;
  3181. }
  3182. /**
  3183. * Apply global events to all grids in the model.
  3184. * (only perfectmix currently... :D)
  3185. */
  3186. events() {
  3187. for (let i = 0; i < this.gridmodels.length; i++) {
  3188. if (this.mix) this.gridmodels[i].perfectMix();
  3189. }
  3190. }
  3191. /**
  3192. * Display all the canvases linked to this simulation
  3193. */
  3194. display() {
  3195. for (let i = 0; i < this.canvases.length; i++){
  3196. this.canvases[i].display();
  3197. if(this.canvases[i].recording == true){
  3198. this.captureFrame(this.canvases[i]);
  3199. }
  3200. }
  3201. }
  3202. /**
  3203. * Start the simulation. start() detects whether the user is running the code from the browser or, alternatively,
  3204. * in nodejs. In the browser, a GUI is provided to interact with the model. In nodejs the
  3205. * programmer can simply wait for the result without wasting time on displaying intermediate stuff
  3206. * (which can be slow)
  3207. */
  3208. start() {
  3209. let sim = this; // Caching this, as function animate changes the this-scope to the scope of the animate-function
  3210. let meter = undefined;
  3211. if (this.inbrowser) {
  3212. if(this.fpsmeter){
  3213. meter = new FPSMeter({ position: 'absolute', show: 'fps', left: "auto", top: "45px", right: "25px", graph: 1, history: 20, smoothing: 100});
  3214. }
  3215. if (this.config.noheader != true) document.title = `Cacatoo - ${this.config.title}`;
  3216. var link = document.querySelector("link[rel~='icon']");
  3217. if (!link) { link = document.createElement('link'); link.rel = 'icon'; document.getElementsByTagName('head')[0].appendChild(link); }
  3218. link.href = '../../images/favicon.png';
  3219. 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>`;
  3220. 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>`;
  3221. 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>`;
  3222. if (document.getElementById("footer") != null) document.getElementById("footer").innerHTML += "<h2>Cacatoo is a toolbox to explore spatially structured models straight from your webbrowser. Suggestions or issues can be reported <a href=\"https://github.com/bramvandijk88/cacatoo/issues\">here.</a></h2>";
  3223. let simStartTime = performance.now();
  3224. async function animate() {
  3225. if (sim.sleep > 0) await pause(sim.sleep);
  3226. if(sim.fpsmeter) meter.tickStart();
  3227. if (!sim.pause == true) {
  3228. sim.step();
  3229. sim.events();
  3230. }
  3231. if(sim.time%(sim.skip+1)==0) sim.display();
  3232. if(sim.fpsmeter) meter.tick();
  3233. let frame = requestAnimationFrame(animate);
  3234. if (sim.time > sim.config.maxtime || sim.exit) {
  3235. let simStopTime = performance.now();
  3236. console.log("Cacatoo completed after", Math.round(simStopTime - simStartTime) / 1000, "seconds");
  3237. cancelAnimationFrame(frame);
  3238. }
  3239. //if (sim.pause == true) { cancelAnimationFrame(frame); }
  3240. }
  3241. requestAnimationFrame(animate);
  3242. }
  3243. else {
  3244. while (true) {
  3245. sim.step();
  3246. if (sim.time >= sim.config.maxtime || sim.exit ) {
  3247. console.log("Cacatoo completed.");
  3248. return true;
  3249. }
  3250. }
  3251. }
  3252. }
  3253. /**
  3254. * Stop the simulation
  3255. */
  3256. stop() {
  3257. this.exit = true;
  3258. }
  3259. /**
  3260. * initialGrid populates a grid with states. The number of arguments
  3261. * is flexible and defined the percentage of every state. For example,
  3262. * initialGrid('grid','species',1,0.5,2,0.25) populates the grid with
  3263. * two species (1 and 2), where species 1 occupies 50% of the grid, and
  3264. * species 2 25%.
  3265. * @param {@GridModel} grid The gridmodel containing the grid to be modified.
  3266. * @param {String} property The name of the state to be set
  3267. * @param {integer} value The value of the state to be set (optional argument with position 2, 4, 6, ..., n)
  3268. * @param {float} fraction The chance the grid point is set to this state (optional argument with position 3, 5, 7, ..., n)
  3269. */
  3270. initialGrid(obj,property,defaultvalue=0) {
  3271. let gridmodel = undefined;
  3272. if (obj instanceof Gridmodel) // user passed a gridmodel object
  3273. gridmodel = obj;
  3274. else
  3275. gridmodel = obj.gridmodel;
  3276. if(typeof gridmodel === 'string' || gridmodel instanceof String) // user passed a string
  3277. gridmodel = this[gridmodel];
  3278. let p = property || obj.property || 'val';
  3279. let defaultval = defaultvalue || obj.default;
  3280. for (let x = 0; x < gridmodel.nc; x++) // x are columns
  3281. for (let y = 0; y < gridmodel.nr; y++) // y are rows
  3282. gridmodel.grid[x][y][p] = defaultval;
  3283. let frequencies = obj.frequencies || [];
  3284. console.log(frequencies);
  3285. for (let arg = 3; arg < arguments.length; arg += 2) {
  3286. let value = arguments[arg];
  3287. let fract = arguments[arg + 1];
  3288. frequencies.push([value,fract]);
  3289. }
  3290. for (let x = 0; x < gridmodel.nc; x++) // x are columns
  3291. for (let y = 0; y < gridmodel.nr; y++){ // y are rows
  3292. let rand = this.rng.random();
  3293. let fract = 0;
  3294. for(let k of frequencies){
  3295. fract += k[1];
  3296. if(rand < fract){
  3297. gridmodel.grid[x][y][p] = k[0];
  3298. break;
  3299. }
  3300. }
  3301. }
  3302. }
  3303. /**
  3304. * populateGrid populates a grid with custom individuals.
  3305. * @param {@GridModel} grid The gridmodel containing the grid to be modified.
  3306. * @param {Array} individuals The properties for individuals 1..n
  3307. * @param {Array} freqs The initial frequency of individuals 1..n
  3308. */
  3309. populateGrid(gridmodel,individuals,freqs)
  3310. {
  3311. if(typeof gridmodel === 'string' || gridmodel instanceof String) gridmodel = this[gridmodel];
  3312. if(individuals.length != freqs.length) throw new Error("populateGrid should have as many individuals as frequencies")
  3313. if(freqs.reduce((a, b) => a + b) > 1) throw new Error("populateGrid should not have frequencies that sum up to greater than 1")
  3314. for (let x = 0; x < gridmodel.nc; x++) // x are columns
  3315. for (let y = 0; y < gridmodel.nr; y++){ // y are rows
  3316. for (const property in individuals[0]) {
  3317. gridmodel.grid[x][y][property] = 0;
  3318. }
  3319. let random_number = this.rng.random();
  3320. let sum_freqs = 0;
  3321. for(let n=0; n<individuals.length; n++)
  3322. {
  3323. sum_freqs += freqs[n];
  3324. if(random_number < sum_freqs) {
  3325. gridmodel.grid[x][y] = {...gridmodel.grid[x][y],...JSON.parse(JSON.stringify(individuals[n]))};
  3326. break
  3327. }
  3328. }
  3329. }
  3330. }
  3331. /**
  3332. * initialSpot populates a grid with states. Grid points close to a certain coordinate are set to state value, while
  3333. * other cells are set to the bg-state of 0.
  3334. * @param {@GridModel} grid The gridmodel containing the grid to be modified.
  3335. * @param {String} property The name of the state to be set
  3336. * @param {integer} value The value of the state to be set (optional argument with position 2, 4, 6, ..., n)
  3337. */
  3338. initialSpot(gridmodel, property, value, size, x, y,background_state=false) {
  3339. if(typeof gridmodel === 'string' || gridmodel instanceof String) gridmodel = this[gridmodel];
  3340. let p = property || 'val';
  3341. for (let x = 0; x < gridmodel.nc; x++) // x are columns
  3342. for (let y = 0; y < gridmodel.nr; y++)
  3343. if(background_state) gridmodel.grid[x % gridmodel.nc][y % gridmodel.nr][p] = background_state;
  3344. this.putSpot(gridmodel,property,value,size,x,y);
  3345. }
  3346. /**
  3347. * putSpot sets values at a certain position with a certain radius. Grid points close to a certain coordinate are set to state value, while
  3348. * other cells are set to the bg-state of 0.
  3349. * @param {@GridModel} grid The gridmodel containing the grid to be modified.
  3350. * @param {String} property The name of the state to be set
  3351. * @param {integer} value The value of the state to be set (optional argument with position 2, 4, 6, ..., n)
  3352. * @param {float} fraction The chance the grid point is set to this state (optional argument with position 3, 5, 7, ..., n)
  3353. */
  3354. putSpot(gridmodel, property, value, size, putx, puty) {
  3355. if(typeof gridmodel === 'string' || gridmodel instanceof String) gridmodel = this[gridmodel];
  3356. // Draw a circle
  3357. for (let x = 0; x < gridmodel.nc; x++) // x are columns
  3358. for (let y = 0; y < gridmodel.nr; y++) // y are rows
  3359. {
  3360. if ((Math.pow((x - putx), 2) + Math.pow((y - puty), 2)) < size)
  3361. gridmodel.grid[x % gridmodel.nc][y % gridmodel.nr][property] = value;
  3362. }
  3363. }
  3364. /**
  3365. * populateSpot populates a spot with custom individuals.
  3366. * @param {@GridModel} grid The gridmodel containing the grid to be modified.
  3367. * @param {Array} individuals The properties for individuals 1..n
  3368. * @param {Array} freqs The initial frequency of individuals 1..n
  3369. */
  3370. populateSpot(gridmodel,individuals, freqs,size, putx, puty, background_state=false)
  3371. {
  3372. if(typeof gridmodel === 'string' || gridmodel instanceof String) gridmodel = this[gridmodel];
  3373. let sumfreqs =0;
  3374. if(individuals.length != freqs.length) throw new Error("populateGrid should have as many individuals as frequencies")
  3375. for(let i=0; i<freqs.length; i++) sumfreqs += freqs[i];
  3376. // Draw a circle
  3377. for (let x = 0; x < gridmodel.nc; x++) // x are columns
  3378. for (let y = 0; y < gridmodel.nr; y++) // y are rows
  3379. {
  3380. if ((Math.pow((x - putx), 2) + Math.pow((y - puty), 2)) < size)
  3381. {
  3382. let cumsumfreq = 0;
  3383. let rand = this.rng.random();
  3384. for(let n=0; n<individuals.length; n++)
  3385. {
  3386. cumsumfreq += freqs[n];
  3387. if(rand < cumsumfreq) {
  3388. Object.assign(gridmodel.grid[x % gridmodel.nc][y % gridmodel.nr],individuals[n]);
  3389. break
  3390. }
  3391. }
  3392. }
  3393. else if(background_state) Object.assign(gridmodel.grid[x][y], background_state);
  3394. }
  3395. }
  3396. /**
  3397. * addButton adds a HTML button which can be linked to a function by the user.
  3398. * @param {string} text Text displayed on the button
  3399. * @param {function} func Function to be linked to the button
  3400. */
  3401. addButton(text, func) {
  3402. if (!this.inbrowser) return
  3403. let button = document.createElement("button");
  3404. button.innerHTML = text;
  3405. button.id = text;
  3406. button.addEventListener("click", func, true);
  3407. document.getElementById("form_holder").appendChild(button);
  3408. }
  3409. /**
  3410. * addSlider adds a HTML slider to the DOM-environment which allows the user
  3411. * to modify a model parameter at runtime.
  3412. * @param {string} parameter The name of the (global!) parameter to link to the slider
  3413. * @param {float} [min] Minimal value of the slider
  3414. * @param {float} [max] Maximum value of the slider
  3415. * @param {float} [step] Step-size when modifying
  3416. */
  3417. addSlider(parameter, min = 0.0, max = 2.0, step = 0.01, label) {
  3418. let lab = label || parameter;
  3419. if (!this.inbrowser) return
  3420. if (window[parameter] === undefined) { console.warn(`addSlider: parameter ${parameter} not found. No slider made.`); return; }
  3421. let container = document.createElement("div");
  3422. container.classList.add("form-container");
  3423. let slider = document.createElement("input");
  3424. let numeric = document.createElement("input");
  3425. container.innerHTML += "<div style='width:100%;height:20px;font-size:12px;'><b>" + lab + ":</b></div>";
  3426. // Setting slider variables / handler
  3427. slider.type = 'range';
  3428. slider.classList.add("slider");
  3429. slider.min = min;
  3430. slider.max = max;
  3431. slider.step = step;
  3432. slider.value = window[parameter];
  3433. slider.oninput = function () {
  3434. let value = parseFloat(slider.value);
  3435. window[parameter] = parseFloat(value);
  3436. numeric.value = value;
  3437. };
  3438. // Setting number variables / handler
  3439. numeric.type = 'number';
  3440. numeric.classList.add("number");
  3441. numeric.min = min;
  3442. numeric.max = max;
  3443. numeric.step = step;
  3444. numeric.value = window[parameter];
  3445. numeric.onchange = function () {
  3446. let value = parseFloat(numeric.value);
  3447. if (value > this.max) value = this.max;
  3448. if (value < this.min) value = this.min;
  3449. window[parameter] = parseFloat(value);
  3450. numeric.value = value;
  3451. slider.value = value;
  3452. };
  3453. container.appendChild(slider);
  3454. container.appendChild(numeric);
  3455. document.getElementById("form_holder").appendChild(container);
  3456. }
  3457. /**
  3458. * addCustomSlider adds a HTML slider to the DOM-environment which allows the user
  3459. * to add a custom callback function to a slider
  3460. * @param {function} func The name of the (global!) parameter to link to the slider
  3461. * @param {float} [min] Minimal value of the slider
  3462. * @param {float} [max] Maximum value of the slider
  3463. * @param {float} [step] Step-size when modifying
  3464. */
  3465. addCustomSlider(label,func, min = 0.0, max = 2.0, step = 0.01, default_value=0) {
  3466. let lab = label || func;
  3467. if (!this.inbrowser) return
  3468. if (func === undefined) { console.warn(`addCustomSlider: callback function not defined. No slider made.`); return; }
  3469. let container = document.createElement("div");
  3470. container.classList.add("form-container");
  3471. let slider = document.createElement("input");
  3472. let numeric = document.createElement("input");
  3473. container.innerHTML += "<div style='width:100%;height:20px;font-size:12px;'><b>" + lab + ":</b></div>";
  3474. // Setting slider variables / handler
  3475. slider.type = 'range';
  3476. slider.classList.add("slider");
  3477. slider.min = min;
  3478. slider.max = max;
  3479. slider.step = step;
  3480. slider.value = default_value;
  3481. //let parent = sim
  3482. slider.oninput = function () {
  3483. let value = parseFloat(slider.value);
  3484. func(value);
  3485. numeric.value = value;
  3486. };
  3487. // Setting number variables / handler
  3488. numeric.type = 'number';
  3489. numeric.classList.add("number");
  3490. numeric.min = min;
  3491. numeric.max = max;
  3492. numeric.step = step;
  3493. numeric.value = default_value;
  3494. numeric.onchange = function () {
  3495. let value = parseFloat(numeric.value);
  3496. if (value > this.max) value = this.max;
  3497. if (value < this.min) value = this.min;
  3498. func(value);
  3499. numeric.value = value;
  3500. slider.value = value;
  3501. };
  3502. container.appendChild(slider);
  3503. container.appendChild(numeric);
  3504. document.getElementById("form_holder").appendChild(container);
  3505. }
  3506. /**
  3507. * save a PNG of an entire HTML div element
  3508. * @param {div} div object to store to
  3509. */
  3510. sectionToPNG(div, prefix){
  3511. function downloadURI(uri, filename) {
  3512. var link = document.createElement("a");
  3513. link.download = filename;
  3514. link.href = uri;
  3515. link.click();
  3516. //after creating link you should delete dynamic link
  3517. //clearDynamicLink(link);
  3518. }
  3519. div = document.getElementById(div);
  3520. let time = sim.time+1;
  3521. let timestamp = time.toString();
  3522. timestamp = timestamp.padStart(6, "0");
  3523. html2canvas(div).then(canvas => {
  3524. var myImage = canvas.toDataURL();
  3525. downloadURI(myImage, prefix+timestamp+".png");
  3526. });
  3527. }
  3528. /**
  3529. * recordVideo captures the canvas to an webm-video (browser only)
  3530. * @param {canvas} canvas Canvas object to record
  3531. */
  3532. startRecording(canvas,fps){
  3533. if(!canvas.recording){
  3534. canvas.recording = true;
  3535. canvas.elem.style.outline = '4px solid red';
  3536. sim.capturer = new CCapture( { format: 'webm',
  3537. quality: 100,
  3538. name: `${canvas.label}_starttime_${sim.time}`,
  3539. framerate: fps,
  3540. display: false } );
  3541. sim.capturer.start();
  3542. console.log("Started recording video.");
  3543. }
  3544. }
  3545. captureFrame(canvas){
  3546. if(canvas.recording){
  3547. sim.capturer.capture(canvas.elem);
  3548. }
  3549. }
  3550. stopRecording(canvas){
  3551. if(canvas.recording){
  3552. canvas.recording = false;
  3553. canvas.elem.style.outline = '0px';
  3554. sim.capturer.stop();
  3555. sim.capturer.save();
  3556. console.log("Video saved");
  3557. }
  3558. }
  3559. makeMovie(canvas, fps=60){
  3560. 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).")
  3561. if(!sim.recording){
  3562. sim.startRecording(canvas,fps);
  3563. sim.recording=true;
  3564. }
  3565. else {
  3566. sim.stopRecording(canvas);
  3567. sim.recording=false;
  3568. }
  3569. }
  3570. /**
  3571. * addToggle adds a HTML checkbox element to the DOM-environment which allows the user
  3572. * to flip boolean values
  3573. * @param {string} parameter The name of the (global!) boolean to link to the checkbox
  3574. */
  3575. addToggle(parameter, label, func) {
  3576. let lab = label || parameter;
  3577. if (!this.inbrowser) return
  3578. if (window[parameter] === undefined) { console.warn(`addToggle: parameter ${parameter} not found. No toggle made.`); return; }
  3579. let container = document.createElement("div");
  3580. container.classList.add("form-container");
  3581. let checkbox = document.createElement("input");
  3582. container.innerHTML += "<div style='width:100%;height:20px;font-size:12px;'><b>" + lab + ":</b></div>";
  3583. // Setting variables / handler
  3584. checkbox.type = 'checkbox';
  3585. checkbox.checked = window[parameter];
  3586. checkbox.oninput = function () {
  3587. window[parameter] = checkbox.checked;
  3588. if(func) func();
  3589. };
  3590. container.appendChild(checkbox);
  3591. document.getElementById("form_holder").appendChild(container);
  3592. }
  3593. /**
  3594. * Adds some html to an existing DIV in your web page.
  3595. * @param {String} div Name of DIV to add to
  3596. * @param {String} html HTML code to add
  3597. */
  3598. addHTML(div, html) {
  3599. if (!this.inbrowser) return
  3600. let container = document.createElement("div");
  3601. container.innerHTML += html;
  3602. document.getElementById(div).appendChild(container);
  3603. }
  3604. /**
  3605. * Add a statedrawing posibility to this canvas
  3606. * @param {Gridmodel} gridmodel The gridmodel to which this canvas belongs
  3607. * @param {string} property the property that should be shown on the canvas
  3608. * @param {} value_to_place set @property to this value
  3609. * @param {int} brushsize radius of the brush
  3610. * @param {int} brushflow amounts of substeps taken (1 by default)
  3611. */
  3612. addStatebrush(gridmodel, property_to_change, value_to_place, brushsize, brushflow, canvas)
  3613. {
  3614. if(typeof gridmodel === 'string' || gridmodel instanceof String) gridmodel = this[gridmodel];
  3615. this.mouseDown = false;
  3616. this.coords_previous = [];
  3617. this.coords = [];
  3618. let thissim = this;
  3619. // var intervalfunc
  3620. this.place_value = value_to_place;
  3621. this.place_size = brushsize;
  3622. this.property_to_change = property_to_change;
  3623. this.brushflow = brushflow || 1;
  3624. if(!canvas){
  3625. let canvs = gridmodel.canvases;
  3626. canvas = canvs[Object.keys(canvs)[0]];
  3627. }
  3628. else {
  3629. canvas = gridmodel.canvases[canvas];
  3630. }
  3631. // For mouse:
  3632. canvas.elem.addEventListener('mousemove', (e) => {
  3633. thissim.coords_previous = thissim.coords;
  3634. thissim.coords = sim.getCursorPosition(canvas,e,sim.config.scale);
  3635. });
  3636. canvas.elem.addEventListener('mousedown', (e) => {
  3637. thissim.intervalfunc = setInterval(function() {
  3638. if(thissim.mouseDown){
  3639. let steps = thissim.brushflow;
  3640. if(steps > 1){
  3641. let difx = thissim.coords.x - thissim.coords_previous.x;
  3642. let seqx = Array.from({ length: steps}, (_, i) => Math.round(thissim.coords_previous.x + (i * difx/(steps-1))));
  3643. let dify = thissim.coords.y - thissim.coords_previous.y;
  3644. let seqy = Array.from({ length: steps}, (_, i) => Math.round(thissim.coords_previous.y + (i * dify/(steps-1))));
  3645. for(let q=0; q<steps; q++)
  3646. {
  3647. thissim.putSpot(gridmodel, thissim.property_to_change, thissim.place_value, thissim.place_size, seqx[q], seqy[q]);
  3648. }
  3649. }
  3650. else {
  3651. thissim.putSpot(gridmodel, thissim.property_to_change, thissim.place_value, thissim.place_size, thissim.coords.x, thissim.coords.y);
  3652. }
  3653. canvas.displaygrid();
  3654. }
  3655. }, 10);
  3656. });
  3657. canvas.elem.addEventListener('mousedown', (e) => { thissim.mouseDown = true; });
  3658. canvas.elem.addEventListener('mouseup', (e) => { thissim.mouseDown = false; });
  3659. // For touch screens
  3660. canvas.elem.addEventListener('touchmove', (e) => {
  3661. thissim.coords_previous = thissim.coords;
  3662. thissim.coords = sim.getCursorPosition(canvas,e.touches[0],sim.config.scale);
  3663. e.preventDefault();
  3664. });
  3665. canvas.elem.addEventListener('touchstart', (e) => {
  3666. thissim.intervalfunc = setInterval(function() {
  3667. if(thissim.mouseDown){
  3668. let steps = thissim.brushflow;
  3669. if(steps > 1){
  3670. let difx = thissim.coords.x - thissim.coords_previous.x;
  3671. let seqx = Array.from({ length: steps}, (_, i) => Math.round(thissim.coords_previous.x + (i * difx/(steps-1))));
  3672. let dify = thissim.coords.y - thissim.coords_previous.y;
  3673. let seqy = Array.from({ length: steps}, (_, i) => Math.round(thissim.coords_previous.y + (i * dify/(steps-1))));
  3674. for(let q=0; q<steps; q++)
  3675. {
  3676. thissim.putSpot(gridmodel, thissim.property_to_change, thissim.place_value, thissim.place_size, seqx[q], seqy[q]);
  3677. }
  3678. }
  3679. else {
  3680. thissim.putSpot(gridmodel, thissim.property_to_change, thissim.place_value, thissim.place_size, thissim.coords.x, thissim.coords.y);
  3681. }
  3682. canvas.displaygrid();
  3683. }
  3684. }, 10);
  3685. });
  3686. canvas.elem.addEventListener('touchstart', (e) => { thissim.mouseDown = true; });
  3687. canvas.elem.addEventListener('touchend', (e) => { thissim.mouseDown = false; });
  3688. }
  3689. /**
  3690. * Add an object-drawing posibility to this canvas
  3691. * @param {Gridmodel} gridmodel The gridmodel to which this canvas belongs
  3692. * @param {object} obj Replace current gp with this object
  3693. * @param {int} brushsize radius of the brush
  3694. * @param {int} brushflow amounts of substeps taken (1 by default)
  3695. * @param {string} canvas alternative canvas name to draw on (first canvas by default)
  3696. */
  3697. addObjectbrush(gridmodel, obj, brushsize, brushflow, canvas)
  3698. {
  3699. if(typeof gridmodel === 'string' || gridmodel instanceof String) gridmodel = this[gridmodel];
  3700. this.mouseDown = false;
  3701. this.coords_previous = [];
  3702. this.coords = [];
  3703. let thissim = this;
  3704. // var intervalfunc
  3705. this.place_size = brushsize;
  3706. this.brushflow = brushflow || 1;
  3707. if(!canvas){
  3708. let canvs = gridmodel.canvases;
  3709. canvas = canvs[Object.keys(canvs)[0]];
  3710. }
  3711. else {
  3712. canvas = gridmodel.canvases[canvas];
  3713. }
  3714. canvas.elem.addEventListener('mousemove', (e) => {
  3715. thissim.coords_previous = thissim.coords;
  3716. thissim.coords = sim.getCursorPosition(canvas,e,sim.config.scale);
  3717. });
  3718. canvas.elem.addEventListener('mousedown', (e) => {
  3719. thissim.intervalfunc = setInterval(function() {
  3720. if(thissim.mouseDown){
  3721. let steps = thissim.brushflow;
  3722. if(steps > 1){
  3723. let difx = thissim.coords.x - thissim.coords_previous.x;
  3724. let seqx = Array.from({ length: steps}, (_, i) => Math.round(thissim.coords_previous.x + (i * difx/(steps-1))));
  3725. let dify = thissim.coords.y - thissim.coords_previous.y;
  3726. let seqy = Array.from({ length: steps}, (_, i) => Math.round(thissim.coords_previous.y + (i * dify/(steps-1))));
  3727. for(let q=0; q<steps; q++)
  3728. {
  3729. thissim.populateSpot(gridmodel, [obj], [1], thissim.place_size, seqx[q], seqy[q]);
  3730. }
  3731. }
  3732. else {
  3733. thissim.populateSpot(gridmodel, [obj], [1], thissim.place_size, thissim.coords.x, thissim.coords.y);
  3734. }
  3735. canvas.displaygrid();
  3736. }
  3737. }, 10);
  3738. });
  3739. canvas.elem.addEventListener('mousedown', (e) => { thissim.mouseDown = true; });
  3740. canvas.elem.addEventListener('mouseup', (e) => { thissim.mouseDown = false; });
  3741. }
  3742. /**
  3743. * log a message to either the console, or to a HTML div.
  3744. * @param {String} msg String to write to log
  3745. * @param {String} target If defined, write log to HTML div with this name
  3746. */
  3747. log(msg, target, append = true) {
  3748. if (!this.inbrowser) console.log(msg);
  3749. else if (typeof target == "undefined") console.log(msg);
  3750. else {
  3751. if(append) document.getElementById(target).innerHTML += `${msg}<br>`;
  3752. else document.getElementById(target).innerHTML = `${msg}<br>`;
  3753. }
  3754. }
  3755. /**
  3756. * write a string to either a file, or generate a download request in the browser
  3757. * @param {String} text String to write
  3758. * @param {String} filename write to this filename
  3759. */
  3760. write(text, filename){
  3761. if (!this.inbrowser) {
  3762. let fs;
  3763. try { fs = require('fs'); }
  3764. catch(e){ console.warn(`[Cacatoo warning] Module 'fs' is not installed. Cannot write to \'${filename}\'. Please run 'npm install fs'`); return }
  3765. fs.writeFileSync(filename, text);
  3766. }
  3767. else {
  3768. var element = document.createElement('a');
  3769. element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text));
  3770. element.setAttribute('download', filename);
  3771. element.style.display = 'none';
  3772. document.body.appendChild(element);
  3773. element.click();
  3774. document.body.removeChild(element);
  3775. }
  3776. }
  3777. /**
  3778. * append a string to a file (only supported in nodejs mode)
  3779. * @param {String} text String to write
  3780. * @param {String} filename write to this filename
  3781. */
  3782. write_append(text, filename){
  3783. if(this.inbrowser)
  3784. {
  3785. console.warn("Cacatoo warning: sorry, appending to files is not supported in browser mode.");
  3786. }
  3787. else {
  3788. let fs;
  3789. try { fs = require('fs'); }
  3790. catch(e){ console.warn(`[Cacatoo warning] Module 'fs' is not installed. Cannot write to \'${filename}\'. Please run 'npm install fs'`); return }
  3791. fs.appendFileSync(filename, text);
  3792. }
  3793. }
  3794. /**
  3795. * Write a gridmodel to a file (only works outside of the browser, useful for running stuff overnight)
  3796. * Defaults to -1 if the property is not set
  3797. * @param {String} msg String to write to log
  3798. * @param {String} target If defined, write log to HTML div with this name
  3799. */
  3800. write_grid(model,property,filename,warn=true) {
  3801. if(this.inbrowser){
  3802. if(warn) {
  3803. // const fs = require('fs');
  3804. // fs.writeFile(filename, 'Hello World!', function (err) {
  3805. // if (err) return console.log(err);
  3806. // console.log('Hello World > helloworld.txt');
  3807. // });
  3808. console.log("Sorry, writing grid files currently works in NODEJS mode only.");
  3809. }
  3810. return
  3811. }
  3812. else {
  3813. const fs = require('fs');
  3814. let string = "";
  3815. for(let x =0; x<model.nc;x++){
  3816. for(let y=0;y<model.nr;y++){
  3817. let prop = model.grid[x][y][property] ? model.grid[x][y][property] : -1;
  3818. string += [x,y,prop].join('\t')+'\n';
  3819. }
  3820. }
  3821. fs.appendFileSync(filename, string);
  3822. }
  3823. }
  3824. /**
  3825. * addMovieButton adds a standard button to record a video
  3826. * @param {@Model} model (@Gridmodel of @Flockmodel) containing the canvas to be recorded.
  3827. * @param {String} property The name of the display (canvas) to be recorded
  3828. *
  3829. */
  3830. addMovieButton(model,canvasname,fps=60){
  3831. const simu = this;
  3832. this.addButton("Record", function() {
  3833. simu.makeMovie(model.canvases[canvasname],fps);
  3834. });
  3835. }
  3836. /**
  3837. * addPatternButton adds a pattern button to the HTML environment which allows the user
  3838. * to load a PNG which then sets the state of 'proparty' for the @GridModel.
  3839. * (currently only supports black and white image)
  3840. * @param {@GridModel} targetgrid The gridmodel containing the grid to be modified.
  3841. * @param {String} property The name of the state to be set
  3842. */
  3843. addPatternButton(targetgrid, property) {
  3844. if (!this.inbrowser) return
  3845. let imageLoader = document.createElement("input");
  3846. imageLoader.type = "file";
  3847. imageLoader.id = "imageLoader";
  3848. let sim = this;
  3849. imageLoader.style = "display:none";
  3850. imageLoader.name = "imageLoader";
  3851. document.getElementById("form_holder").appendChild(imageLoader);
  3852. let label = document.createElement("label");
  3853. label.setAttribute("for", "imageLoader");
  3854. label.style = "background-color: rgb(239, 218, 245);border-radius: 10px;border: 2px solid rgb(188, 141, 201);padding:7px;font-size:10px;margin:10px;width:128px;";
  3855. label.innerHTML = "Select your own initial state";
  3856. document.getElementById("form_holder").appendChild(label);
  3857. let canvas = document.createElement('canvas');
  3858. canvas.name = "imageCanvas";
  3859. let ctx = canvas.getContext('2d');
  3860. function handleImage(e) {
  3861. let reader = new FileReader();
  3862. let grid_data;
  3863. let grid = e.currentTarget.grid;
  3864. reader.onload = function (event) {
  3865. var img = new Image();
  3866. img.onload = function () {
  3867. canvas.width = img.width;
  3868. canvas.height = img.height;
  3869. ctx.drawImage(img, 0, 0);
  3870. grid_data = get2DFromCanvas(canvas);
  3871. for (let x = 0; x < grid.nc; x++) for (let y = 0; y < grid.nr; y++) grid.grid[x][y].alive = 0;
  3872. for (let x = 0; x < grid_data[0].length; x++) // x are columns
  3873. for (let y = 0; y < grid_data.length; y++) // y are rows
  3874. {
  3875. grid.grid[Math.floor(x + grid.nc / 2 - img.width / 2)][Math.floor(y + grid.nr / 2 - img.height / 2)][property] = grid_data[y][x];
  3876. }
  3877. sim.display();
  3878. };
  3879. img.src = event.target.result;
  3880. };
  3881. reader.readAsDataURL(e.target.files[0]);
  3882. document.getElementById("imageLoader").value = "";
  3883. }
  3884. imageLoader.addEventListener('change', handleImage, false);
  3885. imageLoader.grid = targetgrid; // Bind a grid to imageLoader
  3886. }
  3887. /**
  3888. * addCheckpointButton adds a button to the HTML environment which allows the user
  3889. * to reload the grid to the state as found in a JSON file saved by save_grid. The JSON
  3890. * file must of course match the simulation (nrows, ncols, properties in gps), but this
  3891. * is the users own responsibility.
  3892. * @param {@GridModel} targetgrid The gridmodel containing the grid to reload the grid.
  3893. */
  3894. addCheckpointButton(target_model) {
  3895. if (!this.inbrowser) return
  3896. let checkpointLoader = document.createElement("input");
  3897. checkpointLoader.type = "file";
  3898. checkpointLoader.id = "checkpointLoader";
  3899. let sim = this;
  3900. checkpointLoader.style = "display:none";
  3901. checkpointLoader.name = "checkpointLoader";
  3902. document.getElementById("form_holder").appendChild(checkpointLoader);
  3903. let label = document.createElement("label");
  3904. label.setAttribute("for", "checkpointLoader");
  3905. label.style = "background-color: rgb(239, 218, 245);border-radius: 10px;border: 2px solid rgb(188, 141, 201);padding:7px;font-size:10px;margin:10px;width:128px;";
  3906. label.innerHTML = "Reload from checkpoint";
  3907. document.getElementById("form_holder").appendChild(label);
  3908. checkpointLoader.addEventListener('change', function()
  3909. {
  3910. let file_to_read = document.getElementById("checkpointLoader").files[0];
  3911. let name = document.getElementById("checkpointLoader").files[0].name;
  3912. let fileread = new FileReader();
  3913. console.log(`Reloading simulation from checkpoint-file \'${name}\'`);
  3914. fileread.onload = function(e) {
  3915. let content = e.target.result;
  3916. let grid_json = JSON.parse(content); // parse json
  3917. console.log(grid_json);
  3918. let model = sim[target_model];
  3919. model.clearGrid();
  3920. model.grid_from_json(grid_json);
  3921. sim.display();
  3922. };
  3923. fileread.readAsText(file_to_read);
  3924. });
  3925. }
  3926. /**
  3927. * initialPattern takes a @GridModel and loads a pattern from a PNG file. Note that this
  3928. * will only work when Cacatoo is ran on a server due to security issues. If you want to
  3929. * use this feature locally, there are plugins for most browser to host a simple local
  3930. * webserver.
  3931. * (currently only supports black and white image)
  3932. */
  3933. initialPattern(grid, property, image_path, putx, puty) {
  3934. let sim = this;
  3935. if (typeof window != undefined) {
  3936. for (let x = 0; x < grid.nc; x++) for (let y = 0; y < grid.nr; y++) grid.grid[x][y][property] = 0;
  3937. let tempcanv = document.createElement("canvas");
  3938. let tempctx = tempcanv.getContext('2d');
  3939. var tempimg = new Image();
  3940. tempimg.onload = function () {
  3941. tempcanv.width = tempimg.width;
  3942. tempcanv.height = tempimg.height;
  3943. tempctx.drawImage(tempimg, 0, 0);
  3944. let grid_data = get2DFromCanvas(tempcanv);
  3945. if (putx + tempimg.width >= grid.nc || puty + tempimg.height >= grid.nr) throw RangeError("Cannot place pattern outside of the canvas")
  3946. for (let x = 0; x < grid_data[0].length; x++) // x are columns
  3947. for (let y = 0; y < grid_data.length; y++) // y are rows
  3948. {
  3949. grid.grid[putx + x][puty + y][property] = grid_data[y][x];
  3950. }
  3951. sim.display();
  3952. };
  3953. tempimg.src = image_path;
  3954. tempimg.crossOrigin = "anonymous";
  3955. }
  3956. else {
  3957. console.error("initialPattern currently only supported in browser-mode");
  3958. }
  3959. }
  3960. /**
  3961. * Toggle the mix option
  3962. */
  3963. toggle_mix() {
  3964. if (this.mix) this.mix = false;
  3965. else this.mix = true;
  3966. }
  3967. /**
  3968. * Toggle the pause option. Restart the model if pause is disabled.
  3969. */
  3970. toggle_play() {
  3971. if (this.pause) this.pause = false;
  3972. else this.pause = true;
  3973. }
  3974. /**
  3975. * colourRamp interpolates between two arrays to get a smooth colour scale.
  3976. * @param {array} arr1 Array of R,G,B values to start fromtargetgrid The gridmodel containing the grid to be modified.
  3977. * @param {array} arr2 Array of R,B,B values to transition towards
  3978. * @param {integer} n number of steps taken
  3979. * @return {dict} A dictionary (i.e. named JS object) of colours
  3980. */
  3981. colourRamp(arr1, arr2, n) {
  3982. let return_dict = {};
  3983. for (let i = 0; i < n; i++) {
  3984. return_dict[i] = [Math.floor(arr1[0] + arr2[0] * (i / n)),
  3985. Math.floor(arr1[1] + arr2[1] * (i / n)),
  3986. Math.floor(arr1[2] + arr2[2] * (i / n))];
  3987. }
  3988. return return_dict
  3989. }
  3990. }
  3991. /**
  3992. * Below are a few global functions that are used by Simulation classes, but not a method of a Simulation instance per se
  3993. */
  3994. //Delay for a number of milliseconds
  3995. const pause = (timeoutMsec) => new Promise(resolve => setTimeout(resolve, timeoutMsec));
  3996. /**
  3997. * Reconstruct a 2D array based on a canvas
  3998. * @param {canvas} canvas HTML canvas element to convert to a 2D grid for Cacatoo
  3999. * @return {2DArray} Returns a 2D array (i.e. a grid) with the states
  4000. */
  4001. function get2DFromCanvas(canvas) {
  4002. let width = canvas.width;
  4003. let height = canvas.height;
  4004. let ctx = canvas.getContext('2d');
  4005. let img1 = ctx.getImageData(0, 0, width, height);
  4006. let binary = new Array(img1.data.length);
  4007. let idx = 0;
  4008. for (var i = 0; i < img1.data.length; i += 4) {
  4009. let num = [img1.data[i], img1.data[i + 1], img1.data[i + 2]];
  4010. let state;
  4011. if (JSON.stringify(num) == JSON.stringify([0, 0, 0])) state = 0;
  4012. else if (JSON.stringify(num) == JSON.stringify([255, 255, 255])) state = 1;
  4013. else if (JSON.stringify(num) == JSON.stringify([255, 0, 0])) state = 2;
  4014. else if (JSON.stringify(num) == JSON.stringify([0, 0, 255])) state = 3;
  4015. else throw RangeError("Colour in your pattern does not exist in Cacatoo")
  4016. binary[idx] = state;
  4017. idx++;
  4018. }
  4019. const arr2D = [];
  4020. let rows = 0;
  4021. while (rows < height) {
  4022. arr2D.push(binary.splice(0, width));
  4023. rows++;
  4024. }
  4025. return arr2D
  4026. }
  4027. try
  4028. {
  4029. module.exports = Simulation;
  4030. }
  4031. catch(err)
  4032. {
  4033. // do nothing
  4034. }