// HELPERS window.structuredClone = typeof (structuredClone) == "function" ? structuredClone : function (value) { return JSON.parse(JSON.stringify(value)); }; /** * Utility class providing various helper functions for OpenLIME. * Includes methods for SVG manipulation, file loading, image processing, and string handling. * * * @static */ class Util { /** * Pads a number with leading zeros * @param {number} num - Number to pad * @param {number} size - Desired string length * @returns {string} Zero-padded number string * * @example * ```javascript * Util.padZeros(42, 5); // Returns "00042" * ``` */ static padZeros(num, size) { num = num.toString(); while (num.length < size) num = "0" + num; return num; } /** * Prints source code with line numbers * Useful for shader debugging * @param {string} str - Source code to print * @private */ static printSrcCode(str) { let i = 1; let result = ''; for (let l of str.split(/\r\n|\r|\n/)) { const nline = Util.padZeros(i, 5); result += nline + ' ' + l + '\n'; i++; } console.log(result); } /** * Creates an SVG element with optional attributes * @param {string} tag - SVG element tag name * @param {Object} [attributes] - Key-value pairs of attributes * @returns {SVGElement} Created SVG element * * @example * ```javascript * const circle = Util.createSVGElement('circle', { * cx: '50', * cy: '50', * r: '40' * }); * ``` */ static createSVGElement(tag, attributes) { let e = document.createElementNS('http://www.w3.org/2000/svg', tag); if (attributes) for (const [key, value] of Object.entries(attributes)) e.setAttribute(key, value); return e; } /** * Parses SVG string into DOM element * @param {string} text - SVG content string * @returns {SVGElement} Parsed SVG element * @throws {Error} If parsing fails */ static SVGFromString(text) { const parser = new DOMParser(); return parser.parseFromString(text, "image/svg+xml").documentElement; } /** * Loads SVG file from URL * @param {string} url - URL to SVG file * @returns {Promise<SVGElement>} Loaded and parsed SVG * @throws {Error} If fetch fails or content isn't SVG * * @example * ```javascript * const svg = await Util.loadSVG('icons/icon.svg'); * document.body.appendChild(svg); * ``` */ static async loadSVG(url) { let response = await fetch(url); if (!response.ok) { const message = `An error has occured: ${response.status}`; throw new Error(message); } let data = await response.text(); let result = null; if (Util.isSVGString(data)) { result = Util.SVGFromString(data); } else { const message = `${url} is not an SVG file`; throw new Error(message); } return result; }; /** * Loads HTML content from URL * @param {string} url - URL to HTML file * @returns {Promise<string>} HTML content * @throws {Error} If fetch fails */ static async loadHTML(url) { let response = await fetch(url); if (!response.ok) { const message = `An error has occured: ${response.status}`; throw new Error(message); } let data = await response.text(); return data; }; /** * Loads and parses JSON from URL * @param {string} url - URL to JSON file * @returns {Promise<Object>} Parsed JSON data * @throws {Error} If fetch or parsing fails */ static async loadJSON(url) { let response = await fetch(url); if (!response.ok) { const message = `An error has occured: ${response.status}`; throw new Error(message); } let data = await response.json(); return data; } /** * Loads image from URL * @param {string} url - Image URL * @returns {Promise<HTMLImageElement>} Loaded image * @throws {Error} If image loading fails */ static async loadImage(url) { return new Promise((resolve, reject) => { const img = new Image(); img.addEventListener('load', () => resolve(img)); img.addEventListener('error', (err) => reject(err)); img.src = url; }); } /** * Appends loaded image to container * @param {HTMLElement} container - Target container * @param {string} url - Image URL * @param {string} [imgClass] - Optional CSS class * @returns {Promise<void>} */ static async appendImg(container, url, imgClass = null) { const img = await Util.loadImage(url); if (imgClass) img.classList.add(imgClass); container.appendChild(img); return img; } /** * Appends multiple images to container * @param {HTMLElement} container - Target container * @param {string[]} urls - Array of image URLs * @param {string} [imgClass] - Optional CSS class * @returns {Promise<void>} */ static async appendImgs(container, urls, imgClass = null) { for (const u of urls) { const img = await Util.loadImage(u); if (imgClass) img.classList.add(imgClass); container.appendChild(img); } } /** * Tests if string is valid SVG content * @param {string} input - String to test * @returns {boolean} True if string is valid SVG */ static isSVGString(input) { const regex = /^\s*(?:<\?xml[^>]*>\s*)?(?:<!doctype svg[^>]*\s*(?:\[?(?:\s*<![^>]*>\s*)*\]?)*[^>]*>\s*)?(?:<svg[^>]*>[^]*<\/svg>|<svg[^/>]*\/\s*>)\s*$/i if (input == undefined || input == null) return false; input = input.toString().replace(/\s*<!Entity\s+\S*\s*(?:"|')[^"]+(?:"|')\s*>/img, ''); input = input.replace(/<!--([\s\S]*?)-->/g, ''); return Boolean(input) && regex.test(input); } /** * Computes Signed Distance Field from image data * Implementation based on Felzenszwalb & Huttenlocher algorithm * * @param {Uint8Array} buffer - Input image data * @param {number} w - Image width * @param {number} h - Image height * @param {number} [cutoff=0.25] - Distance field cutoff * @param {number} [radius=8] - Maximum distance to compute * @returns {Float32Array|Array} Computed distance field * * Technical Details: * - Uses 2D Euclidean distance transform * - Separate inner/outer distance fields * - Optimized grid computation * - Sub-pixel accuracy */ static computeSDF(buffer, w, h, cutoff = 0.25, radius = 8) { // 2D Euclidean distance transform by Felzenszwalb & Huttenlocher https://cs.brown.edu/~pff/dt/ function edt(data, width, height, f, d, v, z) { for (let x = 0; x < width; x++) { for (let y = 0; y < height; y++) { f[y] = data[y * width + x] } edt1d(f, d, v, z, height) for (let y = 0; y < height; y++) { data[y * width + x] = d[y] } } for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { f[x] = data[y * width + x] } edt1d(f, d, v, z, width) for (let x = 0; x < width; x++) { data[y * width + x] = Math.sqrt(d[x]) } } } // 1D squared distance transform function edt1d(f, d, v, z, n) { v[0] = 0; z[0] = -INF z[1] = +INF for (let q = 1, k = 0; q < n; q++) { var s = ((f[q] + q * q) - (f[v[k]] + v[k] * v[k])) / (2 * q - 2 * v[k]) while (s <= z[k]) { k-- s = ((f[q] + q * q) - (f[v[k]] + v[k] * v[k])) / (2 * q - 2 * v[k]) } k++ v[k] = q z[k] = s z[k + 1] = +INF } for (let q = 0, k = 0; q < n; q++) { while (z[k + 1] < q) k++ d[q] = (q - v[k]) * (q - v[k]) + f[v[k]] } } var data = new Uint8ClampedArray(buffer); const INF = 1e20; const size = Math.max(w, h); // temporary arrays for the distance transform const gridOuter = Array(w * h); const gridInner = Array(w * h); const f = Array(size); const d = Array(size); const z = Array(size + 1); const v = Array(size); for (let i = 0; i < w * h; i++) { var a = data[i] / 255.0; gridOuter[i] = a === 1 ? 0 : a === 0 ? INF : Math.pow(Math.max(0, 0.5 - a), 2); gridInner[i] = a === 1 ? INF : a === 0 ? 0 : Math.pow(Math.max(0, a - 0.5), 2); } edt(gridOuter, w, h, f, d, v, z) edt(gridInner, w, h, f, d, v, z) const dist = window.Float32Array ? new Float32Array(w * h) : new Array(w * h) for (let i = 0; i < w * h; i++) { dist[i] = Math.min(Math.max(1 - ((gridOuter[i] - gridInner[i]) / radius + cutoff), 0), 1) } return dist; } /** * Rasterizes SVG to ImageData * @param {string} url - SVG URL * @param {number[]} [size=[64,64]] - Output dimensions [width, height] * @returns {Promise<ImageData>} Rasterized image data * * Processing steps: * 1. Loads SVG file * 2. Sets up canvas context * 3. Handles aspect ratio * 4. Centers image * 5. Renders to ImageData * * @example * ```javascript * const imageData = await Util.rasterizeSVG('icon.svg', [128, 128]); * context.putImageData(imageData, 0, 0); * ``` */ static async rasterizeSVG(url, size = [64, 64]) { const svg = await Util.loadSVG(url); const svgWidth = svg.getAttribute('width'); const svgHeight = svg.getAttribute('height'); const canvas = document.createElement("canvas"); canvas.width = size[0]; canvas.height = size[1]; svg.setAttributeNS(null, 'width', `100%`); svg.setAttributeNS(null, 'height', `100%`); const ctx = canvas.getContext("2d"); const data = (new XMLSerializer()).serializeToString(svg); const DOMURL = window.URL || window.webkitURL || window; const img = new Image(); const svgBlob = new Blob([data], { type: 'image/svg+xml;charset=utf-8' }); const svgurl = DOMURL.createObjectURL(svgBlob); img.src = svgurl; return new Promise((resolve, reject) => { img.onload = () => { const aCanvas = size[0] / size[1]; const aSvg = svgWidth / svgHeight; let wSvg = 0; let hSvg = 0; if (aSvg < aCanvas) { hSvg = size[1]; wSvg = hSvg * aSvg; } else { wSvg = size[0]; hSvg = wSvg / aSvg; } let dy = (size[1] - hSvg) * 0.5; let dx = (size[0] - wSvg) * 0.5; ctx.translate(dx, dy); ctx.drawImage(img, 0, 0); DOMURL.revokeObjectURL(svgurl); const imageData = ctx.getImageData(0, 0, size[0], size[1]); // const imgURI = canvas // .toDataURL('image/png') // .replace('image/png', 'image/octet-stream'); // console.log(imgURI); resolve(imageData); }; img.onerror = (e) => reject(e); }); } } /** * Implementation Notes: * * File Loading: * - Consistent error handling across loaders * - Promise-based async operations * - Resource cleanup (URL revocation) * * SVG Processing: * - Namespace-aware element creation * - Robust SVG validation * - Attribute management * * Image Processing: * - Canvas-based rasterization * - Aspect ratio preservation * - Memory efficient operations * * SDF Computation: * - Efficient distance field generation * - Configurable parameters * - TypedArray support * * Error Handling: * - Input validation * - Descriptive error messages * - Resource cleanup on failure * * Browser Compatibility: * - Fallbacks for older browsers * - Polyfill for structuredClone * - Vendor prefix handling */ export { Util }