On this page

# domUtils.js

    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?

Type anything to view search result