agentscript
Version:
AgentScript Model in Model/View architecture
1,201 lines (1,089 loc) • 65.5 kB
HTML
<!DOCTYPE html><html lang="en" style="font-size:16px"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="icon" href="../favicon.ico"><title>Source: utils.js</title><!--[if lt IE 9]>
<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
<![endif]--><script src="scripts/third-party/hljs.js" defer="defer"></script><script src="scripts/third-party/hljs-line-num.js" defer="defer"></script><script src="scripts/third-party/popper.js" defer="defer"></script><script src="scripts/third-party/tippy.js" defer="defer"></script><script src="scripts/third-party/tocbot.min.js"></script><script>var baseURL="/",locationPathname="";baseURL=(locationPathname=document.location.pathname).substr(0,locationPathname.lastIndexOf("/")+1)</script><link rel="stylesheet" href="styles/clean-jsdoc-theme.min.css"><svg aria-hidden="true" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="display:none"><defs><symbol id="copy-icon" viewbox="0 0 488.3 488.3"><g><path d="M314.25,85.4h-227c-21.3,0-38.6,17.3-38.6,38.6v325.7c0,21.3,17.3,38.6,38.6,38.6h227c21.3,0,38.6-17.3,38.6-38.6V124 C352.75,102.7,335.45,85.4,314.25,85.4z M325.75,449.6c0,6.4-5.2,11.6-11.6,11.6h-227c-6.4,0-11.6-5.2-11.6-11.6V124 c0-6.4,5.2-11.6,11.6-11.6h227c6.4,0,11.6,5.2,11.6,11.6V449.6z"/><path d="M401.05,0h-227c-21.3,0-38.6,17.3-38.6,38.6c0,7.5,6,13.5,13.5,13.5s13.5-6,13.5-13.5c0-6.4,5.2-11.6,11.6-11.6h227 c6.4,0,11.6,5.2,11.6,11.6v325.7c0,6.4-5.2,11.6-11.6,11.6c-7.5,0-13.5,6-13.5,13.5s6,13.5,13.5,13.5c21.3,0,38.6-17.3,38.6-38.6 V38.6C439.65,17.3,422.35,0,401.05,0z"/></g></symbol><symbol id="search-icon" viewBox="0 0 512 512"><g><g><path d="M225.474,0C101.151,0,0,101.151,0,225.474c0,124.33,101.151,225.474,225.474,225.474 c124.33,0,225.474-101.144,225.474-225.474C450.948,101.151,349.804,0,225.474,0z M225.474,409.323 c-101.373,0-183.848-82.475-183.848-183.848S124.101,41.626,225.474,41.626s183.848,82.475,183.848,183.848 S326.847,409.323,225.474,409.323z"/></g></g><g><g><path d="M505.902,476.472L386.574,357.144c-8.131-8.131-21.299-8.131-29.43,0c-8.131,8.124-8.131,21.306,0,29.43l119.328,119.328 c4.065,4.065,9.387,6.098,14.715,6.098c5.321,0,10.649-2.033,14.715-6.098C514.033,497.778,514.033,484.596,505.902,476.472z"/></g></g></symbol><symbol id="font-size-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M11.246 15H4.754l-2 5H.6L7 4h2l6.4 16h-2.154l-2-5zm-.8-2L8 6.885 5.554 13h4.892zM21 12.535V12h2v8h-2v-.535a4 4 0 1 1 0-6.93zM19 18a2 2 0 1 0 0-4 2 2 0 0 0 0 4z"/></symbol><symbol id="add-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M11 11V5h2v6h6v2h-6v6h-2v-6H5v-2z"/></symbol><symbol id="minus-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M5 11h14v2H5z"/></symbol><symbol id="dark-theme-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M10 7a7 7 0 0 0 12 4.9v.1c0 5.523-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2h.1A6.979 6.979 0 0 0 10 7zm-6 5a8 8 0 0 0 15.062 3.762A9 9 0 0 1 8.238 4.938 7.999 7.999 0 0 0 4 12z"/></symbol><symbol id="light-theme-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M12 18a6 6 0 1 1 0-12 6 6 0 0 1 0 12zm0-2a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM11 1h2v3h-2V1zm0 19h2v3h-2v-3zM3.515 4.929l1.414-1.414L7.05 5.636 5.636 7.05 3.515 4.93zM16.95 18.364l1.414-1.414 2.121 2.121-1.414 1.414-2.121-2.121zm2.121-14.85l1.414 1.415-2.121 2.121-1.414-1.414 2.121-2.121zM5.636 16.95l1.414 1.414-2.121 2.121-1.414-1.414 2.121-2.121zM23 11v2h-3v-2h3zM4 11v2H1v-2h3z"/></symbol><symbol id="reset-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M18.537 19.567A9.961 9.961 0 0 1 12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10c0 2.136-.67 4.116-1.81 5.74L17 12h3a8 8 0 1 0-2.46 5.772l.997 1.795z"/></symbol><symbol id="down-icon" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M12.7803 6.21967C13.0732 6.51256 13.0732 6.98744 12.7803 7.28033L8.53033 11.5303C8.23744 11.8232 7.76256 11.8232 7.46967 11.5303L3.21967 7.28033C2.92678 6.98744 2.92678 6.51256 3.21967 6.21967C3.51256 5.92678 3.98744 5.92678 4.28033 6.21967L8 9.93934L11.7197 6.21967C12.0126 5.92678 12.4874 5.92678 12.7803 6.21967Z"></path></symbol><symbol id="codepen-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M16.5 13.202L13 15.535v3.596L19.197 15 16.5 13.202zM14.697 12L12 10.202 9.303 12 12 13.798 14.697 12zM20 10.869L18.303 12 20 13.131V10.87zM19.197 9L13 4.869v3.596l3.5 2.333L19.197 9zM7.5 10.798L11 8.465V4.869L4.803 9 7.5 10.798zM4.803 15L11 19.131v-3.596l-3.5-2.333L4.803 15zM4 13.131L5.697 12 4 10.869v2.262zM2 9a1 1 0 0 1 .445-.832l9-6a1 1 0 0 1 1.11 0l9 6A1 1 0 0 1 22 9v6a1 1 0 0 1-.445.832l-9 6a1 1 0 0 1-1.11 0l-9-6A1 1 0 0 1 2 15V9z"/></symbol><symbol id="close-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M12 10.586l4.95-4.95 1.414 1.414-4.95 4.95 4.95 4.95-1.414 1.414-4.95-4.95-4.95 4.95-1.414-1.414 4.95-4.95-4.95-4.95L7.05 5.636z"/></symbol><symbol id="menu-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M3 4h18v2H3V4zm0 7h18v2H3v-2zm0 7h18v2H3v-2z"/></symbol></defs></svg></head><body data-theme="dark"><div class="sidebar-container"><div class="sidebar" id="sidebar"><a href="/" class="sidebar-title sidebar-title-anchor">Home</a><div class="sidebar-items-container"><div class="sidebar-section-title with-arrow" data-isopen="false" id="sidebar-classes"><div>Classes</div><svg><use xlink:href="#down-icon"></use></svg></div><div class="sidebar-section-children-container"><div class="sidebar-section-children"><a href="AgentArray.html">AgentArray</a></div><div class="sidebar-section-children"><a href="AgentList.html">AgentList</a></div><div class="sidebar-section-children"><a href="AgentSet.html">AgentSet</a></div><div class="sidebar-section-children"><a href="Animator.html">Animator</a></div><div class="sidebar-section-children"><a href="DataSet.html">DataSet</a></div><div class="sidebar-section-children"><a href="GUI.html">GUI</a></div><div class="sidebar-section-children"><a href="GeoDataSet.html">GeoDataSet</a></div><div class="sidebar-section-children"><a href="Keyboard.html">Keyboard</a></div><div class="sidebar-section-children"><a href="Link.html">Link</a></div><div class="sidebar-section-children"><a href="Links.html">Links</a></div><div class="sidebar-section-children"><a href="Model.html">Model</a></div><div class="sidebar-section-children"><a href="Model3D.html">Model3D</a></div><div class="sidebar-section-children"><a href="Mouse.html">Mouse</a></div><div class="sidebar-section-children"><a href="Patch.html">Patch</a></div><div class="sidebar-section-children"><a href="Patches.html">Patches</a></div><div class="sidebar-section-children"><a href="RGBDataSet.html">RGBDataSet</a></div><div class="sidebar-section-children"><a href="ThreeDraw.html">ThreeDraw</a></div><div class="sidebar-section-children"><a href="Turtle.html">Turtle</a></div><div class="sidebar-section-children"><a href="Turtle3D.html">Turtle3D</a></div><div class="sidebar-section-children"><a href="Turtles.html">Turtles</a></div><div class="sidebar-section-children"><a href="TwoDraw.html">TwoDraw</a></div><div class="sidebar-section-children"><a href="World.html">World</a></div></div><div class="sidebar-section-title with-arrow" data-isopen="false" id="sidebar-modules"><div>Modules</div><svg><use xlink:href="#down-icon"></use></svg></div><div class="sidebar-section-children-container"><div class="sidebar-section-children"><a href="module-src_geojson.html">src/geojson</a></div><div class="sidebar-section-children"><a href="module-src_gis.html">src/gis</a></div><div class="sidebar-section-children"><a href="module-src_utils.html">src/utils</a></div></div><div class="sidebar-section-title with-arrow" data-isopen="false" id="sidebar-tutorials"><div>Tutorials</div><svg><use xlink:href="#down-icon"></use></svg></div><div class="sidebar-section-children-container"><div class="sidebar-section-children"><a href="tutorial-1 - AgentScript.html">1 - AgentScript</a></div><div class="sidebar-section-children"><a href="tutorial-2 - JavaScript.html">2 - JavaScript</a></div><div class="sidebar-section-children"><a href="tutorial-3 - Browser.html">3 - Browser</a></div><div class="sidebar-section-children"><a href="tutorial-4 - Model.html">4 - Model</a></div><div class="sidebar-section-children"><a href="tutorial-5 - View.html">5 - View</a></div><div class="sidebar-section-children"><a href="tutorial-6.1 - AnimatorController.html">6.1 - AnimatorController</a></div><div class="sidebar-section-children"><a href="tutorial-6.2 - KeyboardController.html">6.2 - KeyboardController</a></div><div class="sidebar-section-children"><a href="tutorial-6.3 - GuiController.html">6.3 - GuiController</a></div><div class="sidebar-section-children"><a href="tutorial-6.4 - MouseController.html">6.4 - MouseController</a></div><div class="sidebar-section-children"><a href="tutorial-7 - CodePenServer.html">7 - CodePenServer</a></div></div></div></div></div><div class="navbar-container" id="VuAckcnZhf"><nav class="navbar"><div class="navbar-left-items"><div class="navbar-item"><a id="github" href="https://github.com/backspaces/agentscript" target="">Github</a></div><div class="navbar-item"><a id="AgentScript" href="https://code.agentscript.org" target="">AgentScript.org</a></div><div class="navbar-item"><a id="npm" href="https://www.npmjs.com/package/agentscript" target="">npm</a></div><div class="navbar-item"><a id="unpkg" href="https://unpkg.com/browse/agentscript/" target="">unpkg</a></div></div><div class="navbar-right-items"><div class="navbar-right-item"><button class="icon-button search-button" aria-label="open-search"><svg><use xlink:href="#search-icon"></use></svg></button></div><div class="navbar-right-item"><button class="icon-button theme-toggle" aria-label="toggle-theme"><svg><use class="theme-svg-use" xlink:href="#light-theme-icon"></use></svg></button></div><div class="navbar-right-item"><button class="icon-button font-size" aria-label="change-font-size"><svg><use xlink:href="#font-size-icon"></use></svg></button></div></div><nav></nav></nav></div><div class="toc-container"><div class="toc-content"><span class="bold">On this page</span><div id="eed4d2a0bfd64539bb9df78095dec881"></div></div></div><div class="body-wrapper"><div class="main-content"><div class="main-wrapper"><section id="source-page" class="source-page"><header><h1 id="title" class="has-anchor">utils.js</h1></header><article><pre class="prettyprint source lang-js"><code>/** @module */
// xxxx Beginning of jsUtil
// note Deno has "window" alas!
export function inMain() {
return globalThis.document !== undefined
}
export function inWorker() {
// This should work for browser and deno
return globalThis.WorkerGlobalScope !== undefined
}
export function inNode() {
return typeof globalThis.require !== 'undefined'
}
export function inDeno() {
return typeof globalThis.Deno !== 'undefined'
}
export function hasCanvas() {
return globalThis.canvas !== 'undefined'
}
// ### Async & I/O
// Like Function but is async
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AsyncFunction
export function AsyncFunction(argsArray, fcnBody) {
const ctor = Object.getPrototypeOf(async function () {}).constructor
const asyncFcn = new ctor(...argsArray, fcnBody)
return asyncFcn
}
// Async convert blob to one of three types:
// Type can be one of Text, ArrayBuffer, DataURL
// Camel case ok: text, arrayBuffer, dataURL
// export async function blobToData(blob, type = 'dataURL') {
export function blobToData(blob, type = 'dataURL') {
type = type[0].toUpperCase() + type.slice(1)
const types = ['Text', 'ArrayBuffer', 'DataURL']
if (!types.includes(type))
throw Error('blobToData: data must be one of ' + types.toString())
const reader = new FileReader()
return new Promise((resolve, reject) => {
reader.addEventListener('load', () => resolve(reader.result))
reader.addEventListener('error', e => reject(e))
reader['readAs' + type](blob)
})
}
// deprecated, use: await fetch(url).then(res => res.<type>())
// Async Fetch of a url with the response of the given type
// types: arrayBuffer, blob, json, text; default is blob
// See https://developer.mozilla.org/en-US/docs/Web/API/Response#methods
export async function fetchData(url, type = 'blob') {
const types = ['arrayBuffer', 'blob', 'json', 'text']
if (!types.includes(type))
throw Error('fetchData: data must be one of ' + types.toString())
return fetch(url).then(res => res[type]())
}
export async function fetchJson(url) {
return fetchData(url, 'json')
}
export async function fetchText(url) {
return fetchData(url, 'text')
}
// see fetchImage/ImageBitmap(url) for using the DOM Image in domUtils
// Return a dataURL for the given data. type is a mime type: https://t.ly/vzKm
// If data is a canvas, return data.toDataURL(type), defaulting to image/png
// Otherwise, use btoa/base64, default type text/plain;charset=US-ASCII
export function toDataURL(data, type = undefined) {
if (data.toDataURL) return data.toDataURL(type, type)
if (!type) type = 'text/plain;charset=US-ASCII'
return `data:${type};base64,${btoa(data)}}`
}
export async function blobsEqual(blob0, blob1) {
const text0 = await blob0.text()
const text1 = await blob1.text()
return text0 === text1
}
/**
* Return promise for pause of ms.
* Use: await pause(ms)
*
* @param {number} [ms=1000] Number of ms to pause
* @returns {Promise} A promise to wait this number of ms
*/
// export function timeoutPromise(ms = 1000) {
export function pause(ms = 1000) {
return new Promise(resolve => {
setTimeout(resolve, ms)
})
}
/**
* Use pause for an animation loop.
* Calls the fcn each step
* Stops after steps calls, negative means run forever
*
* @param {function} fcn The function to be called.
* @param {number} [steps=-1] How many times, -1 means forever
* @param {number} [ms=0] Number of ms between calls.
*/
export async function timeoutLoop(fcn, steps = -1, ms = 0) {
let i = 0
while (i++ !== steps) {
fcn(i - 1)
await pause(ms)
}
}
export function waitUntilDone(done, ms = 10) {
return new Promise(resolve => {
function waitOnDone() {
if (done()) return resolve()
else setTimeout(waitOnDone, ms)
}
waitOnDone()
})
}
// ### Debug
let skipChecks = true // fix!
// Function expected a number, got -1.7766780716149864,4.371793401308164,-14.268209308169977
export function checkArg(arg, type = 'number', name = 'Function') {
// // console.log(arg, type, name)
// if (skipChecks) return
// const argType = typeof arg
// if (argType !== type) {
// throw new Error(`${name} expected a ${type}, got ${arg}`)
// }
}
export function checkArgs(argsArray, type = 'number', name = 'Function') {
// if (skipChecks) return
// if (typeOf(argsArray) === 'arguments') argsArray = Array.from(argsArray)
// // console.log(argsArray, type, name)
// argsArray.forEach(val => {
// checkArg(val, type, name)
// })
}
// Print a message just once.
const logOnceMsgSet = new Set()
export function logOnce(msg, useWarn = false) {
if (!logOnceMsgSet.has(msg)) {
if (useWarn) {
console.warn(msg)
} else {
console.log(msg)
}
logOnceMsgSet.add(msg)
}
}
export function warn(msg) {
logOnce(msg, true)
}
// Use chrome/ffox/ie console.time()/timeEnd() performance functions
export function timeit(f, runs = 1e5, name = 'test') {
name = name + '-' + runs
console.time(name)
for (let i = 0; i < runs; i++) f(i)
console.timeEnd(name)
}
// simple performance function.
// Records start & current time, steps, fps
// Each call bumps steps, current time, fps
// Use:
// const perf = fps()
// while (perf.steps != 100) {}
// model.step()
// perf()
// }
// console.log(`Done, steps: ${perf.steps} fps: ${perf.fps}`)
export function fps() {
const timer = typeof performance === 'undefined' ? Date : performance
// const start = performance.now()
const start = timer.now()
let steps = 0
function perf() {
steps++
// const ms = performance.now() - start
const ms = timer.now() - start
const fps = parseFloat((steps / (ms / 1000)).toFixed(2))
Object.assign(perf, { fps, ms, start, steps })
}
perf.steps = 0
return perf
}
// Print Prototype Stack: see your vars all the way down!
export function pps(obj, title = '') {
if (title) console.log(title) // eslint-disable-line
let count = 1
let str = ''
while (obj) {
if (typeof obj === 'function') {
str = obj.constructor.toString()
} else {
const okeys = Object.keys(obj)
str =
okeys.length > 0
? `[${okeys.join(', ')}]`
: `[${obj.constructor.name}]`
}
console.log(`[${count++}]: ${str}`)
obj = Object.getPrototypeOf(obj)
}
}
export function logAll(obj) {
Object.keys(obj).forEach(key => console.log(' ', key, obj[key]))
}
export function cssTrace(
elementName,
names = ['position', 'cursor', 'display', 'width', 'height']
) {
let element = document.querySelector(elementName)
while (element) {
const styles = window.getComputedStyle(element)
console.log('element:', element)
// console.log('styles:', styles)
console.log('tag:', element.tagName)
names.forEach(name => console.log(name + ':', styles[name]))
// console.log('Position:', styles.position)
// console.log('Top:', styles.top)
// console.log('Left:', styles.left)
// console.log('Display:', styles.display)
console.log('------------------------')
element = element.parentElement // Move up to the parent
}
}
// ### Dom
// ### Math
// const { PI, floor, cos, sin, atan2, log, log2, sqrt } = Math
export const PI = Math.PI
/**
* Returns ramdp, int in [0, max), equal or grater than 0, less than max
*
* @param {number} max The max integer to return
* @returns {number} an integer in [0, max)
*/
export function randomInt(max) {
return Math.floor(Math.random() * max)
}
/**
* Returns an int in [min, max), equal or grater than min, less than max
*
* @param {number} min The min integer to return
* @param {number} max The max integer to return
* @returns {number} an integer in [min, max)
*/
export function randomInt2(min, max) {
return min + Math.floor(Math.random() * (max - min))
}
/**
* Returns a random float in [0, max)
*
* @param {number} max The max float to return
* @returns {number} a float in [0, max)
*/
export function randomFloat(max) {
return Math.random() * max
}
/**
* Returns a random float in [min, max)
*
* @param {number} min The min float to return
* @param {number} max The max float to return
* @returns {number} a float in [min, max)
*/
export function randomFloat2(min, max) {
return min + Math.random() * (max - min)
}
/**
* Return a random float centered around r, in [-r/2, r/2)
* @param {number} r The center float
* @returns {number} a float in [-r/2, r/2)
*/
export function randomCentered(r) {
return randomFloat2(-r / 2, r / 2)
}
// Return float Gaussian normal with given mean, std deviation.
export function randomNormal(mean = 0.0, sigma = 1.0) {
// Box-Muller
const [u1, u2] = [1.0 - Math.random(), Math.random()] // ui in 0,1
const norm = Math.sqrt(-2.0 * Math.log(u1)) * Math.cos(2.0 * PI * u2)
return norm * sigma + mean
}
/**
* Install a seeded random generator as Math.random
* Uses an optimized version of the Park-Miller PRNG.
*
* Math.random will return a sequence of "random" numbers in a known
* sequence. Useful for testing to see if the same results
* occur in multiple runs of a model with the same parameters.
*
* @param {number} [seed=123456]
*/
export function randomSeed(seed = 123456) {
// doesn't repeat b4 JS dies.
// https://gist.github.com/blixt/f17b47c62508be59987b
seed = seed % 2147483647
Math.random = () => {
seed = (seed * 16807) % 2147483647
return (seed - 1) / 2147483646
}
}
/**
* Round a number to be of a given decimal precision.
* If the number is an array, round each item in the array
*
* @param {number|Array} num The number to convert/shorten
* @param {number} [digits=4] The number of decimal digits
* @returns {number} The resulting number
*/
export function precision(num, digits = 4) {
// if (num === -0) return 0 // deno doesn't like this
if (Math.abs(num) === 0) return 0
if (Array.isArray(num)) return num.map(val => precision(val, digits))
const mult = 10 ** digits
return Math.round(num * mult) / mult
}
// Return whether num is [Power of Two](http://goo.gl/tCfg5). Very clever!
export const isPowerOf2 = num => (num & (num - 1)) === 0 // twgl library
// Return next greater power of two. There are faster, see:
// [Stack Overflow](https://goo.gl/zvD78e)
export const nextPowerOf2 = num => Math.pow(2, Math.ceil(Math.log2(num)))
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Remainder
// The modulus is defined as: x - y * floor(x / y)
// It is not %, the remainder function.
/**
* A true modulus function, differing from the % remainder operation.
*
* @param {number} v The value to calculate the modulus of
* @param {number} n The number relative to which the modulus is calculated.
* @returns {number} The value of v mod n
*/
export function mod(v, n) {
return ((v % n) + n) % n // v - n * Math.floor(v / n)
}
// Wrap v around min, max values if v outside min, max
export const wrap = (v, min, max) => min + mod(v - min, max - min)
/**
* Clamp a float to be between [min, max).
*
* @param {number} v value to clamp between min & max
* @param {number} min min value
* @param {number} max max value
* @returns {number} a float between min/max
*/
export function clamp(v, min, max) {
if (v < min) return min
if (v > max) return max
return v
}
// Return true is val in [min, max] enclusive
export const isBetween = (val, min, max) => min <= val && val <= max
// Return a linear interpolation between lo and hi.
// Scale is in [0-1], a percentage, and the result is in [lo,hi]
// If lo>hi, scaling is from hi end of range.
// [Why the name `lerp`?](http://goo.gl/QrzMc)
export const lerp = (lo, hi, scale) =>
lo <= hi ? lo + (hi - lo) * scale : lo - (lo - hi) * scale
// Calculate the lerp scale given lo/hi pair and a number between them.
// Clamps number to be between lo & hi.
export function lerpScale(number, lo, hi) {
if (lo === hi) throw Error('lerpScale: lo === hi')
number = clamp(number, lo, hi)
return (number - lo) / (hi - lo)
}
// ### Geometry
// Degrees & Radians
// Note: quantity, not coord system xfm
// const toDegrees = 180 / PI
// const toRadians = PI / 180
export const toDeg = 180 / Math.PI
export const toRad = Math.PI / 180
/**
* Convert from degrees to radians
*
* @param {number} degrees a value in degrees: in [0, 360)
* @returns {number} the value as radians: in [0, 2PI)
*/
export function degToRad(degrees) {
return mod2pi(degrees * toRad)
}
/**
* Convert from radians to degrees
*
* @param {number} radians a value in radians: in [0, 2PI)
* @returns {number} the value as degrees: in [0, 360)
*/
export function radToDeg(radians) {
return mod360(radians * toDeg)
}
// export const radToDeg = radians => mod360(radians * toDeg)
// Wow. surprise: headingToDeg = degToHeading! Just like above.
// deg is absolute eucledian degrees direction
export const degToHeading = degrees => mod360(90 - degrees)
export const headingToDeg = heading => mod360(90 - heading)
export function mod360(degrees) {
return mod(degrees, 360)
}
export function mod2pi(radians) {
return mod(radians, 2 * PI)
}
// export function modpipi(radians) {
// return mod(radians, 2 * PI) - PI
// }
export function mod180180(degrees) {
let theta = mod360(degrees)
if (theta > 180) theta -= 360
return theta
}
// Heading & Radians: coord system
// * Heading is 0-up (y-axis), clockwise angle measured in degrees.
// * Rad is euclidean: 0-right (x-axis), counterclockwise in radians
/**
* Convert from radians to heading
*
* Heading is 0-up (y-axis), clockwise angle measured in degrees.
* Radians is euclidean: 0-right (x-axis), counterclockwise in radians
*
* @param {number} radians a value in radians: in [0, 2PI)
* @returns {number} a value in degrees: in [0, 360)
*/
export function radToHeading(radians) {
const deg = radians * toDeg
return mod360(90 - deg)
}
/**
* Convert from heading to radians
*
* @param {number} heading a value in degrees: in [0, 360)
* @returns {number} a value in radians: in [0, 2PI)
*/
export function headingToRad(heading) {
const deg = mod360(90 - heading)
return deg * toRad
}
// Relative angles in heading space: deg Heading => -deg Eucledian
export function radToHeadingAngle(radians) {
return -radToDeg(radians)
}
export function headingAngleToRad(headingAngle) {
return -degToRad(headingAngle)
}
// headingsEq === degreesEq
export function degreesEqual(deg1, deg2) {
return mod360(deg1) === mod360(deg2)
}
export function radsEqual(rads1, rads2) {
return mod2pi(rads1) === mod2pi(rads2)
}
export const headingsEq = degreesEqual
// Return angle (radians) in (-pi,pi] that added to rad0 = rad1
// See NetLogo's [subtract-headings](http://goo.gl/CjoHuV) for explanation
export function subtractRadians(rad1, rad0) {
let dr = mod2pi(rad1 - rad0) // - PI
if (dr > PI) dr = dr - 2 * PI
return dr
}
// Above using headings (degrees) returning degrees in (-180, 180]
export function subtractDegrees(deg1, deg0) {
let dAngle = mod360(deg1 - deg0) // - 180
if (dAngle > 180) dAngle = dAngle - 360
return dAngle
}
/**
* Subtract two headings, returning the smaller difference.
*
* Computes the difference between the given headings, that is,
* the number of degrees in the smallest angle by which heading2
* could be rotated to produce heading1
* See NetLogo's [subtract-headings](http://goo.gl/CjoHuV) for explanation
* @param {number} head1 The first heading in degrees
* @param {number} head0 The second heading in degrees
* @returns {number} The smallest andle from head0 to head1
*/
export function subtractHeadings(head1, head0) {
return -subtractDegrees(head1, head0)
}
// Return angle in [-pi,pi] radians from (x,y) to (x1,y1)
// [See: Math.atan2](http://goo.gl/JS8DF)
export function radiansTowardXY(x, y, x1, y1) {
return Math.atan2(y1 - y, x1 - x)
}
// Above using headings (degrees) returning degrees in [-90, 90]
export function headingTowardXY(x, y, x1, y1) {
return radToHeading(radiansTowardXY(x, y, x1, y1))
}
// Above using degrees returning degrees in [-90, 90]
export function degreesTowardXY(x, y, x1, y1) {
return radToDeg(radiansTowardXY(x, y, x1, y1))
}
// Return true if x,y is within cone.
// Cone: origin x0,y0 in direction angle, with coneAngle width in radians.
// All angles in radians
export function inCone(x, y, radius, coneAngle, direction, x0, y0) {
if (sqDistance(x0, y0, x, y) > radius * radius) return false
const angle12 = radiansTowardXY(x0, y0, x, y) // angle from 1 to 2
return coneAngle / 2 >= Math.abs(subtractRadians(direction, angle12))
}
// AltAz: Alt is deg from xy plane, 180 up, -180 down, Az is heading
// We choose Phi radians from xy plane, "math" is often from Z axis
// REMIND: some prefer -90, 90
// export function altAzToAnglePhi(alt, az) {
// const angle = headingToRad(az)
// const phi = modpipi(alt * toRad)
// return [angle, phi]
// }
// export function anglePhiToAltAz(angle, phi) {
// const az = radToHeading(angle)
// const alt = mod180180(phi * toDeg)
// return [alt, az]
// }
// Return distance between (x, y), (x1, y1)
export const sqDistance = (x, y, x1, y1) => (x - x1) ** 2 + (y - y1) ** 2
export const distance = (x, y, x1, y1) => Math.sqrt(sqDistance(x, y, x1, y1))
export const sqDistance3 = (x, y, z, x1, y1, z1) =>
(x - x1) ** 2 + (y - y1) ** 2 + (z - z1) ** 2
export const distance3 = (x, y, z, x1, y1, z1) =>
Math.sqrt(sqDistance3(x, y, z, x1, y1, z1))
// ### Models
// model can be:
// - a path to the class Model. uses Model.default. careful! use import.meta
// - a class Model
// - a model
// if either of the first two, they're converted to a model
// the model is called w/ default, no args.. i.e. model()
// async arg: if true use timeoutLoop, otherwise a simple for loop
export async function runModel(model, steps = 500, useSeed = true) {
console.log('runModel: model', model)
if (useSeed) randomSeed()
// if passing in a url, remember that util.js is in the src/ scope
if (isString(model)) model = (await import(model)).default
if (isFunction(model)) model = new model() // model is a class
// console.log('runModel model', model)
await model.startup()
model.setup()
if (inMain()) {
await timeoutLoop(() => {
model.step()
}, steps)
} else {
for (let i = 0; i < steps; i++) model.step()
}
return model
}
// From Model
// needsStartup() {
// // return this.hasOwnProperty('startUp')
// // const proto = Object.getPrototypeOf(this)
// // // console.log('proto', proto)
// // // window.proto = proto
// // return proto.startUp !== undefined
// return new Model().startup !== this.startup
// }
export function classHasStartup(Class) {
console.log('classHasStartup?', Class)
const str = Class.toString()
let lines = str.split('\n')
lines = lines.filter(line => !/^ *\/\//.test(line))
lines = lines.filter(line => /startup\(\)/.test(line))
// console.log('classHasStartup', lines.length > 0)
return lines.length > 0
}
export function toJSON(obj, indent = 0, topLevelArrayOK = true) {
let firstCall = topLevelArrayOK
const blackList = ['rectCache']
const json = JSON.stringify(
obj,
(key, val) => {
if (blackList.includes(key)) {
// if (key === 'rectCache') return val.length
return undefined
}
const isAgentArray =
Array.isArray(val) &&
val.length > 0 &&
Number.isInteger(val[0].id)
if (isAgentArray && !firstCall) {
return val.map(v => v.id)
}
firstCall = false
return val
},
indent
)
return json
}
/**
* Return an object with samples of the models components.
* Useful for testing Models without needing a View, just data.
*
* @param {Model} model A model to sample
* @returns {Object} An object with all the samples
*/
export function sampleModel(model) {
const obj = {
ticks: model.ticks,
model: Object.keys(model),
patches: model.patches.length,
patch: model.patches.oneOf(),
turtles: model.turtles.length,
turtle: model.turtles.oneOf(),
links: model.links.length,
link: model.links.oneOf(),
}
const json = toJSON(obj)
return JSON.parse(json)
}
// ### Arrays, Objects and Iteration
// Three handy functions for defaults & properties
// Identity fcn, returning its argument unchanged. Used in callbacks
export const identityFcn = o => o
// No-op function, does nothing. Used for default callback.
export const noopFcn = () => {}
// Return function returning an object's property. Property in fcn closure.
export const propFcn = prop => o => o[prop]
export function arraysEqual(a1, a2) {
if (a1.length !== a2.length) return false
for (let i = 0; i < a1.length; i++) {
// if (a1[i] !== a2[i]) console.log('arraysEqual: unequal at', i)
if (a1[i] !== a2[i]) return false
}
return true
}
export function removeArrayItem(array, item) {
const ix = array.indexOf(item)
if (ix !== -1) {
array.splice(ix, 1)
} else {
throw Error(`removeArrayItem: ${item} not in array`)
}
// else console.log(`removeArrayItem: ${item} not in array`)
return array // for chaining
}
// Return a string representation of an array of arrays
export const arraysToString = arrays => arrays.map(a => `${a}`).join(',')
// Execute fcn for all own member of an obj or array (typed OK).
// Return input arrayOrObj, transformed by fcn.
// - Unlike forEach, does not skip undefines.
// - Like map, forEach, etc, fcn = fcn(item, key/index, obj).
// - Alternatives are: `for..of`, array map, reduce, filter etc
export function forLoop(arrayOrObj, fcn) {
if (arrayOrObj.slice) {
// typed & std arrays
for (let i = 0, len = arrayOrObj.length; i < len; i++) {
fcn(arrayOrObj[i], i, arrayOrObj)
}
} else {
// obj
Object.keys(arrayOrObj).forEach(k => fcn(arrayOrObj[k], k, arrayOrObj))
}
}
// Repeat function f(i, a) n times, i in 0, n-1
// a is optional array, default a new Array.
// Return a.
/**
* Repeats the function f(i, a) i in 0, n-1.
* a is an optional array, default a new empty Array
* returns a, only needed if f() places data in a
*
* @param {number} n An integer number of times to run f()
* @param {function} f The function called.
* @param {Array} [a=[]] An optional array for use by f()
* @returns {Array} N results of calling f() n times
*/
export function repeat(n, f, a = []) {
for (let i = 0; i < n; i++) f(i, a)
return a
}
// Repeat function n times, incrementing i by step each call.
export function step(n, step, f) {
for (let i = 0; i < n; i += step) f(i)
}
// Return range [0, length-1]. Note: 6x faster than Array.from!
export function range(length) {
return repeat(length, (i, a) => {
a[i] = i
})
}
export function grep(array, regex) {
return array.reduce((acc, val) => {
if (regex.test(val)) acc.push(val)
return acc
}, [])
}
// Return a new array that is the concatination two arrays.
// The resulting Type is that of the first array.
export function concatArrays(array1, array2) {
const Type = array1.constructor
if (Type === Array) {
return array1.concat(convertArrayType(array2, Array))
}
const array = new Type(array1.length + array2.length)
// NOTE: typedArray.set() allows any Array or TypedArray arg
array.set(array1)
array.set(array2, array1.length)
return array
}
// Convert obj to string via JSON. Use indent = 0 for one-liner
// jsKeys true removes the jsonKeys quotes
export function objectToString(obj, indent = 2, jsKeys = true) {
let str = JSON.stringify(obj, null, indent)
if (jsKeys) str = str.replace(/"([^"]+)":/gm, '$1:')
return str
}
export function objectLength(obj) {
return Object.keys(obj).length
}
// Compare Objects or Arrays via JSON string. Note: TypedArrays !== Arrays
export const objectsEqual = (a, b) => JSON.stringify(a) === JSON.stringify(b)
/**
* Return random item from an array
*
* @param {Array} An array to choose from
* @returns {any} The chosen item
*/
// Return random one of array items.
export function oneOf(array) {
return array[randomInt(array.length)]
}
export function otherOneOf(array, item) {
if (array.length < 2) throw Error('otherOneOf: array.length < 2')
let other
do {
other = oneOf(array)
} while (item === other) // note var use
return other
}
// Random key/val of object
export const oneKeyOf = obj => oneOf(Object.keys(obj))
export const oneValOf = obj => obj[oneKeyOf(obj)]
// You'd think this wasn't necessary, but I always forget. Damn.
// NOTE: this, like sort, sorts in place. Clone array if needed.
export function sortNums(array, ascending = true) {
return array.sort((a, b) => (ascending ? a - b : b - a))
}
// Sort an array of objects w/ fcn(obj) as compareFunction.
// If fcn is a string, convert to propFcn.
export function sortObjs(array, fcn, ascending = true) {
if (typeof fcn === 'string') fcn = propFcn(fcn)
const comp = (a, b) => fcn(a) - fcn(b)
return array.sort((a, b) => (ascending ? comp(a, b) : -comp(a, b)))
}
// Randomize array in-place. Use clone() first if new array needed
// The array is returned for chaining; same as input array.
// See [Durstenfeld / Fisher-Yates-Knuth shuffle](https://goo.gl/mfbdPh)
export function shuffle(array) {
for (let i = array.length - 1; i > 0; i--) {
const j = randomInt(i)
;[array[j], array[i]] = [array[i], array[j]]
// const temp = array[i]
// array[i] = array[j]
// array[j] = temp
}
return array
}
// Set operations on arrays
// union: elements in a1 or a2
export function union(a1, a2) {
return Array.from(new Set(a1.concat(a2)))
}
// intersection: elements in a1 and a2
export function intersection(a1, a2) {
// intersection = new Set([...set1].filter(x => set2.has(x)))
const set2 = new Set(a2)
return a1.filter(x => set2.has(x))
}
// Difference: elements from a1 not in a2
export function difference(a1, a2) {
// difference = new Set([...set1].filter(x => !set2.has(x)))
const set2 = new Set(a2)
return a1.filter(x => !set2.has(x))
}
// Return a "ramp" (array of uniformly ascending/descending floats)
// in [start,stop] with numItems (positive integer > 1).
// OK for start>stop. Will always include start/stop w/in float accuracy.
export function floatRamp(start, stop, numItems) {
// NOTE: start + step*i, where step is (stop-start)/(numItems-1),
// has float accuracy problems, must recalc step each iteration.
if (numItems <= 1) throw Error('floatRamp: numItems must be > 1')
const a = []
for (let i = 0; i < numItems; i++) {
a.push(start + (stop - start) * (i / (numItems - 1)))
}
return a
}
// Integer version of floatRamp, start & stop integers, rounding each element.
// Default numItems yields unit step between start & stop.
export function integerRamp(start, stop, numItems = stop - start + 1) {
return floatRamp(start, stop, numItems).map(a => Math.round(a))
}
// Return an array normalized (lerp) between lo/hi values
// export function normalize(array, lo = 0, hi = 1) {
// const [min, max] = [arrayMin(array), arrayMax(array)]
// const scale = 1 / (max - min)
// return array.map(n => lerp(lo, hi, scale * (n - min)))
// }
// // Return Uint8ClampedArray normalized in 0-255
// export function normalize8(array) {
// return new Uint8ClampedArray(normalize(array, -0.5, 255.5))
// }
// // Return Array normalized to integers in lo-hi
// export function normalizeInt(array, lo, hi) {
// return normalize(array, lo, hi).map(n => Math.round(n))
// }
// get nested property like obj.foo.bar.baz:
// const val = nestedProperty(obj, 'foo.bar.baz')
// Optimized for path length up to 4, else uses path.reduce()
export function nestedProperty(obj, path) {
if (typeof path === 'string') path = path.split('.')
switch (path.length) {
case 1:
return obj[path[0]]
case 2:
return obj[path[0]][path[1]]
case 3:
return obj[path[0]][path[1]][path[2]]
case 4:
return obj[path[0]][path[1]][path[2]][path[3]]
default:
return path.reduce((obj, param) => obj[param], obj)
}
}
export const arrayLast = array => array[array.length - 1]
export const arrayMax = array => array.reduce((a, b) => Math.max(a, b))
export const arrayMin = array => array.reduce((a, b) => Math.min(a, b))
export const arrayExtent = array => [arrayMin(array), arrayMax(array)]
export const arraysDiff = (a1, a2, ifcn = i => i) => {
if (a1.length !== a2.length)
return console.log('lengths differ', a1.length, a2.length)
const diffs = []
for (let i = 0; i < a1.length; i++) {
if (a1[i] !== a2[i]) diffs.push([ifcn(i), a1[i], a2[i]])
}
return diffs
}
export function arrayToMatrix(array, width, height) {
if (array.length !== width * height)
throw Error('arrayToMatrix: length !== width * height')
const matrix = []
for (let i = 0; i < height; i++) {
const row = array.slice(i * width, (i + 1) * width)
matrix.push(row)
}
return matrix
}
export const matrixToArray = matrix => matrix.flat()
// ### OofA/AofO
export function isOofA(data) {
if (!isObject(data)) return false
return Object.values(data).every(v => isTypedArray(v))
}
export function toOofA(aofo, spec) {
const length = aofo.length
const keys = Object.keys(spec)
const oofa = {}
keys.forEach(k => {
oofa[k] = new spec[k](length)
})
forLoop(aofo, (o, i) => {
keys.forEach(key => (oofa[key][i] = o[key]))
})
return oofa
}
export function oofaObject(oofa, i, keys) {
const obj = {}
keys.forEach(key => {
obj[key] = oofa[key][i]
})
return obj
}
export function toAofO(oofa, keys = Object.keys(oofa)) {
const length = oofa[keys[0]].length
const aofo = new Array(length)
forLoop(aofo, (val, i) => {
aofo[i] = oofaObject(oofa, i, keys)
})
return aofo
}
export function oofaBuffers(postData) {
const buffers = []
forLoop(postData, obj => forLoop(obj, a => buffers.push(a.buffer)))
return buffers
}
// ### Types
// Fix the javascript typeof operator https://goo.gl/Efdzk5
export const typeOf = obj =>
({}.toString
.call(obj)
.match(/\s(\w+)/)[1]
.toLowerCase())
export const isType = (obj, string) => typeOf(obj) === string
export const isOneOfTypes = (obj, array) => array.includes(typeOf(obj))
export const isString = obj => isType(obj, 'string')
export const isObject = obj => isType(obj, 'object')
// export const isArray = obj => isType(obj, 'array')
export const isArray = obj => Array.isArray(obj)
export const isNumber = obj => isType(obj, 'number')
// Is a number an integer (rather than a float w/ non-zero fractional part)
export const isInteger = n => Number.isInteger(n)
// export const isFloat = n => isNumber(n) && n % 1 !== 0 // https://goo.gl/6MS0Tm
export const isFunction = obj => isType(obj, 'function')
export const isImage = obj => isType(obj, 'image')
export const isCanvas = obj =>
isOneOfTypes(obj, ['htmlcanvaselement', 'offscreencanvas'])
export const isImageable = obj =>
isOneOfTypes(obj, [
'image',
'htmlimageelement',
'htmlcanvaselement',
'offscreencanvas',
'imagebitmap',
])
// Typed Arrays:
export const isTypedArray = obj => typeOf(obj.buffer) === 'arraybuffer'
export const isUintArray = obj => /^uint.*array$/.test(typeOf(obj))
export const isIntArray = obj => /^int.*array$/.test(typeOf(obj))
export const isFloatArray = obj => /^float.*array$/.test(typeOf(obj))
export const isArrayLike = obj => isArray(obj) || isTypedArray(obj)
export const isColorLikeArray = obj =>
isArrayLike(obj) &&
[3, 4].includes(obj.length) &&
obj.every(
i =>
(isInteger(i) && isBetween(i, 0, 255)) ||
(isNumber(i) && isBetween(i, 0, 1))
)
export function isLittleEndian() {
const d32 = new Uint32Array([0x01020304])
return new Uint8ClampedArray(d32.buffer)[0] === 4
}
// Convert Array or TypedArray to given Type (Array or TypedArray).
// Result same length as array, precision may be lost.
export function convertArrayType(array, Type) {
const Type0 = array.constructor
if (Type0 === Type) return array // return array if already same Type
return Type.from(array) // Use .from (both TypedArrays and Arrays)
}
export function isDataSet(obj) {
// return typeOf(obj) === 'object' && obj.width != null && obj.height != null
return typeOf(obj) === 'object' && obj.width && obj.height && obj.data
}
// Unused:
// export const isWebglArray = obj =>
// Array.isArray(obj) && obj.length === 3 && util.arrayMax(obj) <= 1
// isHtmlElement: obj => /^html.*element$/.test(typeOf(obj))
// isImage: obj => isType(obj, 'image')
// isImageBitmap: obj => isType(obj, 'imagebitmap')
// // Is undefined, null, bool, number, string, symbol
// isPrimitive: obj => obj == null || 'object' != typeof obj
// Return array's type (Array or TypedArray variant)
// typeName: obj => obj.constructor.name
// xxxx End of jsUtil
// xxxx Begnning of domUtils
// import {
// inWorker,
// inMain,
// inDeno,
// typeOf,
// isDataSet,
// isTypedArray,
// isObject,
// step,
// } from './jsUtils.js'
// function inWorker() {
// // return !inNode() && typeof self.window === 'undefined'
// return globalThis.WorkerGlobalScope !== undefined
// }
// ### Beginning of the functions requiring the dom
// ### Async & I/O
// download canvas as png or jpeg. Canvas can be a dataURL.
// quality is default. For lossless jpeg, set to 1
export function downloadCanvas(can, name = 'download.png', quality = null) {
if (!(name.endsWith('.png') || name.endsWith('.jpeg'))) name = name + '.png'
const type = name.endsWith('.png') ? 'image/png' : 'image/jpeg'
const url = typeOf(can) === 'string' ? can : can.toDataURL(type, quality)
const link = document.createElement('a')
link.download = name
link.href = url
link.click()
}
// blobable = ArrayBuffer, ArrayBufferView, Blob, String
// Objects & Arrays too, converted to json
export function downloadBlob(blobable, name = 'download', format = true) {
if (isDataSet(blobable) && !Array.isArray(blobable.data))
blobable.data = Array.from(blobable.data)
if (isTypedArray(blobable)) blobable = Array.from(blobable)
if (isObject(blobable) || Array.isArray(blobable))
blobable = format
? JSON.stringify(blobable, null, 2)
: JSON.stringify(blobable)
const blob = typeOf(blobable) === 'blob' ? blobable : new Blob([blobable])
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.download = name
link.href = url
link.click()
URL.revokeObjectURL(url)
}
export function downloadJson(json, name = 'json.js') {
downloadBlob(json, name)
}
export function downloadJsonModule(json, name = 'json.js') {
const string = JSON.stringify(json, null, 2)
const module = `const json = ${string}
export default json`
downloadBlob(module, name)
}
// ### Canvas & Image
/**
* Return a Promise for getting an image.
* See https://deno.land/x/skia_canvas/mod.ts?s=Image for Image in deno
*
* use: imagePromise('./path/to/img').then(img => imageFcn(img))
* or: await imagePromise('./path/to/img')
*
* @param {string} url URL for path to image
* @returns {Promise} A promise resolving to the image
*/
export async function imagePromise(url, preferDOM = true) {
if ((inMain() && preferDOM) || inDeno()) {
return new Promise((resolve, reject) => {
const img = new Image()
img.crossOrigin = 'Anonymous'
img.onload = () => resolve(img)
img.onerror = () => reject(`Could not load image ${url}`)
img.src = url
})
} else if (inWorker() || !preferDOM) {
// { mode: 'cors' } ?
const blob = await fetch(url).then(response => response.blob())
return createImageBitmap(blob)
}
}
export async function fetchImage(url) {
return new Promise((resolve, reject) => {
const img = new Image()
img.crossOrigin = 'Anonymous'
img.onload = () => resolve(img)
img.onerror = () => reject(`Could not load image ${url}`)
img.src = url
})
}
export async function fetchImageBitmap(url) {
const blob = await fetchData(url, 'blob')
return createImageBitmap(blob)
}
// export function imageSize(img) {
// if (inDeno()) {
// return [img.width(), img.height()]
// } else {
// return [img.width, img.height]
// }
// }
// function offscreenOK() {
// // return !!self.OffscreenCanvas
// // return typeof OffscreenCanvas !== 'undefined'
// return inWorker()
// }
/**
* Create a blank 2D canvas of a given width/height.
*
* @param {number} width The canvas height in pixels
* @param {number} height The canvas width in pixels
* @param {boolean} [preferDOM=false] If false, return "Offscreen" canvas
* @returns {Canvas} The resulting Canvas object
*/
// export function createCanvas(width, height, offscreen = offscreenOK()) {
// if (offscreen) return new OffscreenCanvas(width, height)
// const can = document.createElement('canvas')
// can.width = width
// can.height = height
// return can
// }
export function createCanvas(width, height, preferDOM = true) {
if (inMain() && preferDOM) {
const can = document.createElement('canvas')
can.width = width
can.height = height
return can
} else if (inDeno()) {
return globalThis.createCanvas(width, height)
} else if (inWorker() || !preferDOM) {
return new OffscreenCanvas(width, height)
}
}
/**
* As above, but returing the 2D context object instead of the canvas.
* Note ctx.canvas is the canvas for the ctx, and can be use as an image.
*
* @param {number} width The canvas height in pixels
* @param {number} height The canvas width in pixels
* @param {boolean} [offscreen=offscreenOK()] If true, return "Offscreen" canvas
* @returns {Context2D} The resulting Canvas's 2D context
*/
expo