agentscript
Version:
AgentScript Model in Model/View architecture
1,177 lines (1,075 loc) • 74 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"><title>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 class="light" data-theme="light"><div class="sidebar-container"><div class="sidebar" id="sidebar"><a href="/" class="sidebar-title sidebar-title-anchor">AgentScript</a><div class="sidebar-items-container"><div class="sidebar-section-title with-arrow" data-isopen="false" id="iCCj9Q0PGmZSEt0A4MU5M"><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-geojson.html">geojson</a></div><div class="sidebar-section-children"><a href="module-gis.html">gis</a></div><div class="sidebar-section-children"><a href="module-steg.html">steg</a></div><div class="sidebar-section-children"><a href="module-utils.html">utils</a></div></div><div class="sidebar-section-title with-arrow" data-isopen="false" id="K8t8Gm76SVDZr6QdSnx5x"><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="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="RUj_moBov1umep5XE4XoP"><div>Namespaces</div><svg><use xlink:href="#down-icon"></use></svg></div><div class="sidebar-section-children-container"><div class="sidebar-section-children"><a href="Color.html">Color</a></div><div class="sidebar-section-children"><a href="ColorMap.html">ColorMap</a></div></div><div class="sidebar-section-title with-arrow" data-isopen="false" id="9l-2YtveDFnHrdRyo9tpb"><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-Leaflet.html">Leaflet</a></div><div class="sidebar-section-children"><a href="tutorial-MVC.html">MVC</a></div><div class="sidebar-section-children"><a href="tutorial-Map Libre.html">Map Libre</a></div><div class="sidebar-section-children"><a href="tutorial-View 2.5D.html">View 2.5D</a></div><div class="sidebar-section-children"><a href="tutorial-View 2D.html">View 2D</a></div><div class="sidebar-section-children"><a href="tutorial-View 3D.html">View 3D</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="_blank">GitHub</a></div><div class="navbar-item"><a id="npm" href="https://www.npmjs.com/package/agentscript" target="_blank">npm</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="#dark-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>// /** @namespace */
/** @module */
// ### Async & I/O
// Return Promise for getting an image.
// - use: imagePromise('./path/to/img').then(img => imageFcn(img))
/**
* Return a Promise for getting an image.
*
* 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 function imagePromise(url) {
return new Promise((resolve, reject) => {
const img = new Image()
img.crossOrigin = 'Anonymous'
img.onload = () => resolve(img)
// img.onerror = () => reject(Error(`Could not load image ${url}`))
img.onerror = () => reject(`Could not load image ${url}`)
img.src = url
})
}
// // Convert File blob (actually any blob) to Image
// export function blobImagePromise(blob) {
// const url = URL.createObjectURL(blob)
// return imagePromise(url)
// }
// See https://javascript.info/binary for great blob, dataUrl, objUrl discussion.
// https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/createImageBitmap#parameters
// export async function imageBitmapPromise(urlOrBlob, options = {}) {
// // const blob = await xhrPromise(url, 'blob')
// if (typeof urlOrBlob === 'string')
// urlOrBlob = await fetch(urlOrBlob).then(res => res.blob())
// return createImageBitmap(urlOrBlob, options)
// }
// createImageBitmap(img) with lossless parameters.
// img: image, canvas, video, blob, imageData
export async function imageToImageBitmap(img) {
return createImageBitmap(img, {
premultiplyAlpha: 'none',
colorSpaceConversion: 'none',
})
}
// https://stackoverflow.com/questions/52959839/convert-imagebitmap-to-blob
// https://stackoverflow.com/questions/60031536/difference-between-imagebitmap-and-imagedata
// Return an ImageBitmapRenderingContext from the imageBitmap
// The imageBitmap will have transferred its "ownership" to the ctx.canvas.
// The ctx will NOT have the getImageData function.
// Nor will the ctx.canvas be able to getContext('2d')
// The ctx.canvas can be used to draw on a vanilla canvas
// export async function imageBitmapCtx(imageBitmap) {
// const { width, height } = imageBitmap
// const can = createCanvas(width, height)
// // const can = createCanvas(width, height, true)
// const ctx = can.getContext('bitmaprenderer')
// ctx.transferFromImageBitmap(imageBitmap)
// // downloadCanvas(ctx.canvas) // debug
// return ctx // ctx.getImageData(0, 0, width, height)
// }
export async function imageBitmapRendererCtx(imageBitmap) {
const { width, height } = imageBitmap
const can = new OffscreenCanvas(width, height)
const ctx = can.getContext('bitmaprenderer')
ctx.transferFromImageBitmap(imageBitmap)
return ctx // ctx.getImageData(0, 0, width, height)
}
// Use above to create a new canvas filled with the imageBitmap
// export async function imageBitmapCanvas(imageBitmap) {
// // const ctx = await imageBitmapCtx(imageBitmap)
// // return cloneCanvas(ctx.canvas) // Safe from premultiply?
// return imageBitmapCanvasCtx(imageBitmap).canvas
// }
export function imageBitmap2dCtx(imageBitmap) {
const { width, height } = imageBitmap
const can = new OffscreenCanvas(width, height)
const ctx = can.getContext('2d')
ctx.drawImage(imageBitmap, 0, 0)
return ctx
}
export async function imageBitmapData(imageBitmap) {
const ctx = imageBitmap2dCtx(imageBitmap)
return ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height)
}
// Convert canvas.toBlob callback style to a promise
export async function canvasToBlob(can, mimeType = 'png', quality = undefined) {
if (!mimeType.startsWith('image/')) mimeType = 'image/' + mimeType
return new Promise(resolve => {
can.toBlob(blob => resolve(blob), mimeType, quality)
})
}
// ^^ditto for canvasToDataURL()
// export async function imageToBlob(img) {
// return fetch(url).then(res => res[type]())
// }
// export function asyncify(f) {
// let AsyncFunction = Object.getPrototypeOf(async function () {}).constructor
// const f = new AsyncFunction('context', script)
// }
// 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') {
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)
})
}
// 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')
}
// 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
}
// 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 + '.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) || isArray(blobable))
blobable = format
? JSON.stringify(blobable, null, 2)
: JSON.stringify(blobable)
let 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 downloadDataSet(dataSet, name = 'download.txt') {
// if (!Array.isArray(dataSet.data)) dataSet.data = Array.from(dataSet.data)
// const type = name.endsWith('.png') ? 'image/png' : 'image/jpeg'
// const url = typeof can === 'string' ? can : can.toDataURL(type, quality)
// download(url, name)
// }
// Ditto for blobs
// export function downloadBlob(blob, name = 'download.blob') {
// // canvas.toBlob(callback, mimeType, qualityArgument)
// const url = URL.createObjectURL(blob)
// download(url, name)
// URL.revokeObjectURL(url)
// // const link = document.createElement('a')
// // link.download = name
// // link.href = URL.createObjectURL(blob)
// // link.click()
// // URL.revokeObjectURL(link.href)
// }
// Ditto for strings like json, text and so on
// export function downloadString(string, name = 'download.txt') {
// const base64 = 'data:text/plain;base64,' + string
// download(base64, name)
// }
// General download, w/ "legal" href
// export function download(url, name) {
// const link = document.createElement('a')
// link.download = name
// link.href = url
// link.click()
// }
// Return Promise for ajax/xhr data.
// - type: 'arraybuffer', 'blob', 'document', 'json', 'text'.
// - method: 'GET', 'POST'
// - use: xhrPromise('./path/to/data').then(data => dataFcn(data))
/**
* Return Promise for ajax/xhr data.
*
* type: 'arraybuffer', 'blob', 'document', 'json', 'text'.
* method: 'GET', 'POST'
* use: xhrPromise('./path/to/data').then(data => dataFcn(data))
*
* @param {UTL} url A URL path to the data to be retrieved
* @param {string} [type='text'] The type of the data
* @param {string} [method='GET'] The retrieval method
* @returns {any} The resulting data of the given type
*/
export function xhrPromise(url, type = 'text', method = 'GET') {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest()
xhr.open(method, url) // POST mainly for security and large files
xhr.responseType = type
xhr.onload = () => resolve(xhr.response)
xhr.onerror = () =>
reject(Error(`Could not load ${url}: ${xhr.status}`))
xhr.send()
})
}
/**
* 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 above for an animation loop.
// steps < 0: forever (default), steps === 0 is no-op
// Returns a promise for when done. If forever, no need to use it.
/**
* 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.
* @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 waitPromise(done, ms = 10) {
return new Promise(resolve => {
function waitOn() {
if (done()) return resolve()
else setTimeout(waitOn, ms)
}
waitOn()
})
}
// deprecated, use: await fetch(url).then(res => res.<type>())
// https://developer.mozilla.org/en-US/docs/Web/API/Response#methods
// type = "arrayBuffer" "blob" "formData" "json" "text"
// export async function fetchType(url, type = 'text') {
// const response = await fetch(url)
// if (!response.ok) throw Error(`Not found: ${url}`)
// const value = await response[type]()
// return value
// }
// // Similar pair for requestAnimationFrame
// export function rafPromise() {
// return new Promise(resolve => requestAnimationFrame(resolve))
// }
// export async function rafLoop(fcn, steps = -1) {
// let i = 0
// while (i++ !== steps) {
// fcn(i - 1)
// await rafPromise()
// }
// }
//
// ### Canvas
// import { inWorker } from './dom.js'
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} [offscreen=offscreenOK()] If true, 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
}
/**
* 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,
offscreen = offscreenOK(),
attrs = {}
) {
const can = createCanvas(width, height, offscreen)
return can.getContext('2d', attrs)
}
// Duplicate a canvas, preserving it's current image/drawing
export function cloneCanvas(can, offscreen = offscreenOK()) {
const ctx = createCtx(can.width, can.height, offscreen)
ctx.drawImage(can, 0, 0)
return ctx.canvas
}
// Resize a ctx in-place and preserve image.
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.
export function setCanvasSize(can, width, height) {
if (can.width !== width || can.height != height) {
can.width = width
can.height = height
}
}
// 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.setTransform(1, 0, 0, 1, 0, 0) // or ctx.resetTransform()
}
// 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 })
}
// 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()
}
// # 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.
// ctxDrawText: (ctx, string, x, y, color, setIdentity = true) ->
// @setIdentity(ctx) if setIdentity
// ctx.fillStyle = color.css # @colorStr color
// ctx.fillText(string, x, y)
// ctx.restore() if setIdentity
// 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 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
// error checking:
let skipChecks = false
export function skipErrorChecks(bool) {
skipChecks = bool
}
export function checkArg(arg, type = 'number', name = 'Function') {
if (skipChecks) return
if (typeof arg !== 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)
argsArray.forEach((val, i) => {
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)
}
}
/**
* 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(', '))
// if (logToo) {
// Object.keys(obj).forEach(key => console.log(' ', key, obj[key]))
// }
}
export function logAll(obj) {
Object.keys(obj).forEach(key => console.log(' ', key, obj[key]))
}
// Dump model's patches turtles links to window
export function dump(model = window.model) {
let { 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')
}
// export function logHistogram(name, array) {
// // const hist = AgentArray.fromArray(dataset.data).histogram()
// const hist = histogram(array)
// const { min, max } = hist.parameters
// console.log(
// `${name}:`, // name + ':'
// hist.toString(),
// 'min/max:',
// min.toFixed(3),
// max.toFixed(3)
// )
// }
// Use JSON to return pretty, printable string of an object, array, other
// Remove ""s around keys. Will fail on circular structures.
// export function objectToString(obj) {
// return JSON.stringify(obj, null, ' ')
// .replace(/ {2}"/g, ' ')
// .replace(/": /g, ': ')
// }
// // Like above, but a single line for small objects.
// export function objectToString1(obj) {
// return JSON.stringify(obj)
// .replace(/{"/g, '{')
// .replace(/,"/g, ',')
// .replace(/":/g, ':')
// }
// import { isObject } from './types.js' // see printToPage
// ### Dom
// export function fetchCssStyle(url) {
// document.head.innerHTML += `<link rel="stylesheet" href="${url}" type="text/css" />`
// }
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) {
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 += `<style>${css}</style>`
var 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 (var 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())
}
export function inWorker() {
return !inNode() && typeof self.window === 'undefined'
}
export function inNode() {
return typeof global !== 'undefined'
}
export function inDeno() {
return !!Deno
}
// 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 = '<pre>' + msg + '</pre>'
}
msg = '<pre>' + msg + '</pre>'
if (typeof element === 'string') {
element = document.getElementById(element)
}
element.style.fontFamily = 'monospace'
element.innerHTML += msg //+ '<br />'
}
// Get element (i.e. canvas) relative x,y position from event/mouse position.
export function getEventXY(element, evt) {
// http://goo.gl/356S91
const rect = element.getBoundingClientRect()
return [evt.clientX - rect.left, evt.clientY - rect.top]
}
// Convert a function into a worker via blob url.
// Adds generic error handler. Scripts only, not modules.
export function fcnToWorker(fcn) {
const href = document.location.href
const root = href.replace(/\/[^\/]+$/, '/')
const fcnStr = `(${fcn.toString(root)})("${root}")`
const objUrl = URL.createObjectURL(
new Blob([fcnStr], { type: 'text/javascript' })
)
const worker = new Worker(objUrl)
worker.onerror = function (e) {
console.log('Worker ERROR: Line ', e.lineno, ': ', e.message)
}
return worker
}
// export function workerScript(script, worker) {
// const srcBlob = new Blob([script], { type: 'text/javascript' })
// const srcURL = URL.createObjectURL(srcBlob)
// worker.postMessage({ cmd: 'script', url: srcURL })
// }
// Create dynamic `<script>` tag, appending to `<head>`
// <script src="./test/src/three0.js" type="module"></script>
// NOTE: Use import(path) for es6 modules.
// I.e. this is legacy, for umd's only.
// export function loadScript(path, props = {}) {
// const scriptTag = document.createElement('script')
// scriptTag.src = path
// Object.assign(scriptTag, props)
// document.querySelector('head').appendChild(scriptTag)
// }
// export function loadScript(path, props = {}) {
// return new Promise((resolve, reject) => {
// const scriptTag = document.createElement('script')
// scriptTag.onload = () => resolve(scriptTag)
// scriptTag.src = path
// Object.assign(scriptTag, props)
// document.querySelector('head').appendChild(scriptTag)
// })
// }
// ### Math
// const { PI, floor, cos, sin, atan2, log, log2, sqrt } = Math
export const PI = Math.PI
// Return random int/float in [0,max) or [min,max) or [-r/2,r/2)
/**
* Returns an 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)
}
// export const randomInt = max => 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))
}
// export const randomInt2 = (min, max) =>
// 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
}
// export const randomFloat = max => 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)
}
// export const randomFloat2 = (min, max) => 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)
}
// export const randomCentered = r => 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
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)
}
// export const mod = (v, n) => ((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
// Better names and format for arrays. Change above?
/**
* 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)
// 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)
}
// 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) {
return mod360(degrees) - 180
}
// 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
}
// export const subtractHeadings = (head1, head0) =>
// degToHeading(subtractDegrees(headingToDeg(head1), headingToDeg(head0)))
/**
* 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)
}
// export const subtractHeadings = (head1, head0) => -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))
}
// 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]
// }
// export function mod180180(degrees) {
// return mod360(degrees) - 180
// }
// export function modpipi(radians) {
// return mod2pi(radians) - PI
// }
// 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))
// 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))
}
// export const radians = degrees => mod2pi(degrees * toRad)
// export const degrees = radians => mod360(radians * toDeg)
// export function precision(num, digits = 4) {
// const mult = 10 ** digits
// return Math.round(num * mult) / mult
// }
// Two seedable random number generators
// export function randomSeedSin(seed = PI / 4) {
// // ~3.4 million b4 repeat.
// // https://stackoverflow.com/a/19303725/1791917
// return () => {
// const x = Math.sin(seed++) * 10000
// return x - Math.floor(x)
// }
// }
// export function randomSeedParkMiller(seed = 123456) {
// // doesn't repeat b4 JS dies.
// // https://gist.github.com/blixt/f17b47c62508be59987b
// seed = seed % 2147483647
// return () => {
// seed = (seed * 16807) % 2147483647
// return (seed - 1) / 2147483646
// }
// }
// // Replace Math.random with one of these
// export function randomSeed(seed, useParkMiller = true) {
// Math.random = useParkMiller
// ? randomSeedParkMiller(seed)
// : randomSeedSin(seed)
// }
// ### Models
// import { loadScript, inWorker } from './dom.js'
// import { randomSeed } from './math.js'
// import { repeat } from './objects.js'
// import { timeoutLoop } from './async.js'
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 &&