import {
inWorker,
inMain,
inDeno,
typeOf,
isDataSet,
isTypedArray,
isObject,
step,
} from './jsUtils.js'
// function inWorker() {
// // return !inNode() && typeof self.window === 'undefined'
// return globalThis.WorkerGlobalScope !== undefined
// }
// ### Async & I/O
// download canvas as png or jpeg. Canvas can be a dataURL.
// quality is default. For lossless jpeg, set to 1
export function downloadCanvas(can, name = 'download.png', quality = null) {
if (!(name.endsWith('.png') || name.endsWith('.jpeg'))) name = name + '.png'
const type = name.endsWith('.png') ? 'image/png' : 'image/jpeg'
const url = typeOf(can) === 'string' ? can : can.toDataURL(type, quality)
const link = document.createElement('a')
link.download = name
link.href = url
link.click()
}
// blobable = ArrayBuffer, ArrayBufferView, Blob, String
// Objects & Arrays too, converted to json
export function downloadBlob(blobable, name = 'download', format = true) {
if (isDataSet(blobable) && !Array.isArray(blobable.data))
blobable.data = Array.from(blobable.data)
if (isTypedArray(blobable)) blobable = Array.from(blobable)
if (isObject(blobable) || Array.isArray(blobable))
blobable = format
? JSON.stringify(blobable, null, 2)
: JSON.stringify(blobable)
const blob = typeOf(blobable) === 'blob' ? blobable : new Blob([blobable])
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.download = name
link.href = url
link.click()
URL.revokeObjectURL(url)
}
// ### Canvas & Image
/**
* 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 async function imagePromise(url, preferDOM = true) {
// if (inMain() || inDeno()) {
if ((inMain() && preferDOM) || inDeno()) {
return new Promise((resolve, reject) => {
const img = new Image()
img.crossOrigin = 'Anonymous'
img.onload = () => resolve(img)
img.onerror = () => reject(`Could not load image ${url}`)
img.src = url
})
// } else if (inDeno()) {
// // return loadImage(url)
// console.log('inDeno: url', url, 'Image', Image)
// const img = new Image(url) // needs install in deno function
// console.log('inDeno: img', img)
// await pause(1000)
// console.log('inDeno: img', img)
// return img
} else if (inWorker() || !preferDOM) {
// { mode: 'cors' } ?
const blob = await fetch(url).then(response => response.blob())
return createImageBitmap(blob)
}
}
// export function imageSize(img) {
// if (inDeno()) {
// return [img.width(), img.height()]
// } else {
// return [img.width, img.height]
// }
// }
// function offscreenOK() {
// // return !!self.OffscreenCanvas
// // return typeof OffscreenCanvas !== 'undefined'
// return inWorker()
// }
/**
* Create a blank 2D canvas of a given width/height.
*
* @param {number} width The canvas height in pixels
* @param {number} height The canvas width in pixels
* @param {boolean} [preferDOM=false] If false, return "Offscreen" canvas
* @returns {Canvas} The resulting Canvas object
*/
// export function createCanvas(width, height, offscreen = offscreenOK()) {
// if (offscreen) return new OffscreenCanvas(width, height)
// const can = document.createElement('canvas')
// can.width = width
// can.height = height
// return can
// }
export function createCanvas(width, height, preferDOM = true) {
if (inMain() && preferDOM) {
const can = document.createElement('canvas')
can.width = width
can.height = height
return can
} else if (inDeno()) {
return globalThis.createCanvas(width, height)
} else if (inWorker() || !preferDOM) {
return new OffscreenCanvas(width, height)
}
}
/**
* As above, but returing the 2D context object instead of the canvas.
* Note ctx.canvas is the canvas for the ctx, and can be use as an image.
*
* @param {number} width The canvas height in pixels
* @param {number} height The canvas width in pixels
* @param {boolean} [offscreen=offscreenOK()] If true, return "Offscreen" canvas
* @returns {Context2D} The resulting Canvas's 2D context
*/
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 += `<style>${css}</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 = '<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.
// 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?