Source: Util.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
// 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) {
        return num.toString().padStart(size, '0');
    }

    /**
     * Prints source code with line numbers
     * Useful for shader debugging
     * @param {string} str - Source code to print
     * @private
     */
    static printSrcCode(str) {
        let result = '';
        str.split(/\r\n|\r|\n/).forEach((line, i) => {
            const nline = Util.padZeros(i + 1, 5);
            result += `${nline}   ${line}\n`;
        });
        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) {
        const 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 }