<!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 &amp; 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.&lt;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 &lt; runs; i++) f(i)
    console.timeEnd(name)
}

// simple performance function.
// Records start &amp; 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]))
}

// ### 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 &amp; (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 &amp; 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 &lt; 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 &lt;= val &amp;&amp; val &lt;= 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 &lt;= 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 &amp; 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 &amp; 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 &amp; 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 &lt; 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) &amp;&amp;
                val.length > 0 &amp;&amp;
                Number.isInteger(val[0].id)

            if (isAgentArray &amp;&amp; !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 &amp; 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 &lt; 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 &amp; std arrays
        for (let i = 0, len = arrayOrObj.length; i &lt; 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 &lt; 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 &lt; 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 &lt; 2) throw Error('otherOneOf: array.length &lt; 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 &lt;= 1) throw Error('floatRamp: numItems must be > 1')
    const a = []
    for (let i = 0; i &lt; numItems; i++) {
        a.push(start + (stop - start) * (i / (numItems - 1)))
    }
    return a
}
// Integer version of floatRamp, start &amp; stop integers, rounding each element.
// Default numItems yields unit step between start &amp; 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 &lt; 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 &lt; 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')
export const isInteger = n => Number.isInteger(n)
// export const isFloat = n => isNumber(n) &amp;&amp; n % 1 !== 0 // https://goo.gl/6MS0Tm
export const isFunction = obj => isType(obj, 'function')
export const isImage = obj => isType(obj, 'image')

// Is a number an integer (rather than a float w/ non-zero fractional part)
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) &amp;&amp;
    [3, 4].includes(obj.length) &amp;&amp;
    obj.every(
        i =>
            (isInteger(i) &amp;&amp; isBetween(i, 0, 255)) ||
            (isNumber(i) &amp;&amp; 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' &amp;&amp; obj.width != null &amp;&amp; obj.height != null
    return typeOf(obj) === 'object' &amp;&amp; obj.width &amp;&amp; obj.height &amp;&amp; obj.data
}

// Unused:
// export const isWebglArray = obj =>
//     Array.isArray(obj) &amp;&amp; obj.length === 3 &amp;&amp; util.arrayMax(obj) &lt;= 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() &amp;&amp; typeof self.window === 'undefined'
//     return globalThis.WorkerGlobalScope !== undefined
// }

// ### Beginning of the functions requiring the dom

// ### Async &amp; 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 &amp; Arrays too, converted to json
export function downloadBlob(blobable, name = 'download', format = true) {
    if (isDataSet(blobable) &amp;&amp; !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)
}

// ### Canvas &amp; 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() &amp;&amp; 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() &amp;&amp; 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
 */
export function createCtx(width, height, preferDOM = true, attrs = {}) {
    // const can = createCanvas(width, height, offscreen)
    // return can.getContext('2d', attrs)
    const can = createCanvas(width, height, preferDOM)
    const ctx = can.getContext('2d', attrs)
    if (inDeno()) {
        const ctxObj = {
            canvas: can,
        }
        Object.setPrototypeOf(ctxObj, ctx)
        return ctxObj
    } else {
        return ctx
    }
}

// FIX or drop
// Duplicate a canvas, preserving it's current image/drawing
export function cloneCanvas(can, preferDOM = true) {
    const ctx = createCtx(can.width, can.height, preferDOM)
    ctx.drawImage(can, 0, 0)
    return ctx.canvas
}
// Resize a ctx in-place and preserve image. SpriteSheet
export function resizeCtx(ctx, width, height) {
    const copy = cloneCanvas(ctx.canvas)
    ctx.canvas.width = width
    ctx.canvas.height = height
    ctx.drawImage(copy, 0, 0)
}
// // Return new canvas scaled by width, height and preserve image.
// export function resizeCanvas(
//     can,
//     width,
//     height = (width / can.width) * can.height
// ) {
//     const ctx = createCtx(width, height)
//     ctx.drawImage(can, 0, 0, width, height)
//     return ctx.canvas
// }

// Set the ctx/canvas size if differs from width/height.
// It does not install a transform and assumes there is not one currently installed.
// The World object can do that for AgentSets.
// Can move to World
export function setCanvasSize(can, width, height) {
    if (can.width !== width || can.height != height) {
        can.width = width
        can.height = height
    }
}

// export function canvasToImage(can) {
//     var img = new Image()
//     img.src = can.toDataURL()
// }

// Install identity transform for this context.
// Call ctx.restore() to revert to previous transform.
export function setIdentity(ctx) {
    ctx.save() // NOTE: Does not change state, only saves current state.
    ctx.resetTransform() // or ctx.setTransform(1, 0, 0, 1, 0, 0)
}
// Set the text font, align and baseline drawing parameters.
// Ctx can be either a canvas context or a DOM element
// See [reference](http://goo.gl/AvEAq) for details.
// * font is a HTML/CSS string like: "9px sans-serif"
// * align is left right center start end
// * baseline is top hanging middle alphabetic ideographic bottom
export function setTextProperties(
    ctx,
    font,
    textAlign = 'center',
    textBaseline = 'middle'
) {
    Object.assign(ctx, { font, textAlign, textBaseline })
}

// bboxCtx is reused on every call to stringMetrics
// const bboxCtx = createCtx(0, 0)
let bboxCtx
export function stringMetrics(
    string,
    font,
    textAlign = 'center',
    textBaseline = 'middle'
) {
    // bboxCtx ??= createCtx(0, 0)
    if (!bboxCtx) bboxCtx = createCtx(0, 0)
    setTextProperties(bboxCtx, font, textAlign, textBaseline)
    const metrics = bboxCtx.measureText(string)
    metrics.height = // not sure how safe this is but..
        metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent
    return metrics
}

// Draw string of the given color at the xy location, in ctx pixel coords.
// Use setIdentity .. reset if a transform is being used by caller.
export function drawText(ctx, string, x, y, color, useIdentity = true) {
    if (useIdentity) setIdentity(ctx)
    ctx.fillStyle = color.css || color // OK to use Color.typedColor
    ctx.fillText(string, x, y)
    if (useIdentity) ctx.restore()
}

// Return the (complete) ImageData object for this context object
export function ctxImageData(ctx) {
    return ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height)
}

// Return ctx data as an array of typed array rgba colors
export function ctxImageColors(ctx) {
    const typedArray = ctxImageData(ctx).data
    const colors = []
    step(typedArray.length, 4, i => colors.push(typedArray.subarray(i, i + 4)))
    return colors
}

// Return ctx data as an array of Uint32Array rgba pixels
export function ctxImagePixels(ctx) {
    const imageData = ctxImageData(ctx)
    const pixels = new Uint32Array(imageData.data.buffer)
    return pixels
}

// Clear this context using the cssColor.
// If no color or if color === 'transparent', clear to transparent.
export function clearCtx(ctx, cssColor = undefined) {
    const { width, height } = ctx.canvas
    setIdentity(ctx)
    if (!cssColor || cssColor === 'transparent') {
        ctx.clearRect(0, 0, width, height)
    } else {
        cssColor = cssColor.css || cssColor
        ctx.fillStyle = cssColor
        ctx.fillRect(0, 0, width, height)
    }
    ctx.restore()
}

// These image functions use "imagable" objects: Image, ImageBitmap, Canvas ...
// https://developer.mozilla.org/en-US/docs/Web/API/CanvasImageSource

export function imageToCtx(img) {
    // const [width, height] = imageSize(img)
    const { width, height } = img
    const ctx = createCtx(width, height)
    // const ctx = createCtx(img.width, img.height)
    fillCtxWithImage(ctx, img)
    return ctx
}
export function imageToCanvas(img) {
    return imageToCtx(img).canvas
}
// Fill this context with the given image. Will scale image to fit ctx size.
export function fillCtxWithImage(ctx, img) {
    setIdentity(ctx) // set/restore identity
    ctx.drawImage(img, 0, 0, ctx.canvas.width, ctx.canvas.height)
    ctx.restore()
}

/**
 * Fill this context with the given image, resizing it to img size if needed.
 *
 * @param {Context2D} ctx a canvas 2D context
 * @param {Image} img the Image to install in this ctx
 */
export function setCtxImage(ctx, img) {
    setCanvasSize(ctx.canvas, img.width, img.height)
    fillCtxWithImage(ctx, img)
}

// ### Debug

/**
 * Merge a module's obj key/val pairs into to the global/window namespace.
 * Primary use is to make console logging easier when debugging
 * modules.
 *
 * @param {Object} obj Object who's key/val pairs will be installed in window.
 */
export function toWindow(obj) {
    Object.assign(window, obj)
    console.log('toWindow:', Object.keys(obj).join(', '))
}

export function dump(model = window.model) {
    const { patches: ps, turtles: ts, links: ls } = model
    Object.assign(window, { ps, ts, ls })
    window.p = ps.length > 0 ? ps.oneOf() : {}
    window.t = ts.length > 0 ? ts.oneOf() : {}
    window.l = ls.length > 0 ? ls.oneOf() : {}
    console.log('debug: ps, ts, ls, p, t, l dumped to window')
}

// ### Dom

export function addCssLink(url) {
    const link = document.createElement('link')
    link.setAttribute('rel', 'stylesheet')
    link.setAttribute('href', url)
    document.head.appendChild(link)
}
export async function fetchCssStyle(url) {
    if (url.startsWith('../')) {
        console.log('fetchCssStyle relative url', url)
        url = import.meta.resolve(url)
        console.log('        absolute url', url)
    }
    const response = await fetch(url)
    if (!response.ok) throw Error(`fetchCssStyle: Not found: ${url}`)
    const css = await response.text()
    addCssStyle(css)
    return css
}
export function addCssStyle(css) {
    // document.head.innerHTML += `&lt;style>${css}&lt;/style>`
    const style = document.createElement('style')
    style.innerHTML = css
    document.head.appendChild(style)
}

// REST:
// Parse the query, returning an object of key / val pairs.
export function getQueryString() {
    return window.location.search.substr(1)
}
export function parseQueryString(
    // paramsString = window.location.search.substr(1)
    paramsString = getQueryString()
) {
    const results = {}
    const searchParams = new URLSearchParams(paramsString)
    for (const pair of searchParams.entries()) {
        let [key, val] = pair
        if (val.match(/^[0-9.]+$/) || val.match(/^[0-9.]+e[0-9]+$/))
            val = Number(val)
        if (['true', 't', ''].includes(val)) val = true
        if (['false', 'f'].includes(val)) val = false

        results[key] = val
    }
    return results
}
// Merge the querystring into the default parameters
export function RESTapi(parameters) {
    return Object.assign(parameters, parseQueryString())
}

// Print a message to an html element
// Default to document.body if in browser.
// If msg is an object, convert to JSON
// (object canot have cycles etc)
// If element is string, find element by ID
export function printToPage(msg, element = document.body) {
    // if (isObject(msg)) {
    if (typeof msg === 'object') {
        msg = JSON.stringify(msg, null, 2)
        // msg = '&lt;pre>' + msg + '&lt;/pre>'
    }
    msg = '&lt;pre>' + msg + '&lt;/pre>'

    if (typeof element === 'string') {
        element = document.getElementById(element)
    }

    element.style.fontFamily = 'monospace'
    element.innerHTML += msg //+ '&lt;br />'
}

// Get element (i.e. canvas) relative x,y position from event/mouse position.
// http://goo.gl/356S91
export function getEventXY(element, evt) {
    const rect = element.getBoundingClientRect()
    return [evt.clientX - rect.left, evt.clientY - rect.top]
}

// ### Math

// ### Geometry

// ### Models

// ### Arrays, Objects and Iteration

// ### OofA/AofO

// ### Types

// could have some of the types that are dom oriented. TypedArrays too?

// xxxx End of domUtils
</code></pre></article></section><footer class="footer" id="PeOAagUepe"><div class="wrapper">AgentScript version: 0.10.20 using clean-jsdoc-theme</div></footer></div></div></div><div class="search-container" id="PkfLWpAbet" style="display:none"><div class="wrapper" id="iCxFxjkHbP"><button class="icon-button search-close-button" id="VjLlGakifb" aria-label="close search"><svg><use xlink:href="#close-icon"></use></svg></button><div class="search-box-c"><svg><use xlink:href="#search-icon"></use></svg> <input type="text" id="vpcKVYIppa" class="search-input" placeholder="Search..." autofocus></div><div class="search-result-c" id="fWwVHRuDuN"><span class="search-result-c-text">Type anything to view search result</span></div></div></div><div class="mobile-menu-icon-container"><button class="icon-button" id="mobile-menu" data-isopen="false" aria-label="menu"><svg><use xlink:href="#menu-icon"></use></svg></button></div><div id="mobile-sidebar" class="mobile-sidebar-container"><div class="mobile-sidebar-wrapper"><a href="/" class="sidebar-title sidebar-title-anchor">Home</a><div class="mobile-nav-links"><div class="navbar-item"><a id="github-mobile" href="https://github.com/backspaces/agentscript" target="">Github</a></div><div class="navbar-item"><a id="AgentScript-mobile" href="https://code.agentscript.org" target="">AgentScript.org</a></div><div class="navbar-item"><a id="npm-mobile" href="https://www.npmjs.com/package/agentscript" target="">npm</a></div><div class="navbar-item"><a id="unpkg-mobile" href="https://unpkg.com/browse/agentscript/" target="">unpkg</a></div></div><div class="mobile-sidebar-items-c"><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 class="mobile-navbar-actions"><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></div></div><script type="text/javascript" src="scripts/core.min.js"></script><script src="scripts/search.min.js" defer="defer"></script><script src="scripts/third-party/fuse.js" defer="defer"></script><script type="text/javascript">var tocbotInstance=tocbot.init({tocSelector:"#eed4d2a0bfd64539bb9df78095dec881",contentSelector:".main-content",headingSelector:"h1, h2, h3",hasInnerContainers:!0,scrollContainer:".main-content",headingsOffset:130,onClick:bringLinkToView})</script></body></html>