Spaces:
Running
Running
| /* ===== DOM & Image Utilities ===== */ | |
| window.$ = function(sel, parent) { return (parent || document).querySelector(sel); }; | |
| window.$$ = function(sel, parent) { return [...(parent || document).querySelectorAll(sel)]; }; | |
| window.NAIUtils = { | |
| fileToDataUrl(file) { | |
| return new Promise((resolve, reject) => { | |
| const reader = new FileReader(); | |
| reader.onload = () => resolve(reader.result); | |
| reader.onerror = reject; | |
| reader.readAsDataURL(file); | |
| }); | |
| }, | |
| dataUrlToBase64(dataUrl) { | |
| const idx = dataUrl.indexOf(','); | |
| return idx >= 0 ? dataUrl.substring(idx + 1) : dataUrl; | |
| }, | |
| loadImageFromDataUrl(dataUrl) { | |
| return new Promise((resolve, reject) => { | |
| const img = new Image(); | |
| img.onload = () => resolve(img); | |
| img.onerror = reject; | |
| img.src = dataUrl; | |
| }); | |
| }, | |
| async imageToBase64Jpeg(imgElement, maxSize) { | |
| const canvas = document.createElement('canvas'); | |
| let w = imgElement.naturalWidth || imgElement.width; | |
| let h = imgElement.naturalHeight || imgElement.height; | |
| if (maxSize) { | |
| const scale = maxSize / Math.max(w, h); | |
| if (scale < 1) { w = Math.floor(w * scale); h = Math.floor(h * scale); } | |
| } | |
| canvas.width = w; canvas.height = h; | |
| canvas.getContext('2d').drawImage(imgElement, 0, 0, w, h); | |
| return this.dataUrlToBase64(canvas.toDataURL('image/jpeg', 0.95)); | |
| }, | |
| async resizeImageToBase64(dataUrl, maxSize) { | |
| const img = await this.loadImageFromDataUrl(dataUrl); | |
| return this.imageToBase64Jpeg(img, maxSize); | |
| }, | |
| async sha256(str) { | |
| const buf = new TextEncoder().encode(str); | |
| const hash = await crypto.subtle.digest('SHA-256', buf); | |
| return Array.from(new Uint8Array(hash)).map(b => b.toString(16).padStart(2, '0')).join(''); | |
| }, | |
| autoResize(textarea) { | |
| textarea.style.height = 'auto'; | |
| textarea.style.height = Math.min(textarea.scrollHeight, 200) + 'px'; | |
| }, | |
| setupAutoResize(textarea) { | |
| textarea.addEventListener('input', () => this.autoResize(textarea)); | |
| setTimeout(() => this.autoResize(textarea), 0); | |
| }, | |
| createDynSlider(label, id, min, max, step, value) { | |
| const decimals = step < 0.1 ? 2 : 1; | |
| const group = document.createElement('div'); | |
| group.className = 'form-group dyn-slider-group'; | |
| group.innerHTML = ` | |
| <label for="${id}">${label}</label> | |
| <div class="slider-group"> | |
| <input type="range" id="${id}" min="${min}" max="${max}" step="${step}" value="${value}"> | |
| <span id="${id}-val" class="slider-value">${parseFloat(value).toFixed(decimals)}</span> | |
| </div>`; | |
| setTimeout(() => { | |
| const sl = $(`#${id}`); | |
| const valEl = $(`#${id}-val`); | |
| if (sl && valEl) { | |
| sl.addEventListener('input', () => { | |
| valEl.textContent = parseFloat(sl.value).toFixed(decimals); | |
| }); | |
| } | |
| }, 0); | |
| return group; | |
| }, | |
| bindSlider(sliderId, valueId, transform) { | |
| const slider = $(`#${sliderId}`); | |
| const valueEl = $(`#${valueId}`); | |
| if (!slider || !valueEl) return; | |
| const update = () => { | |
| valueEl.textContent = transform ? transform(parseFloat(slider.value)) : slider.value; | |
| }; | |
| slider.addEventListener('input', update); | |
| update(); | |
| }, | |
| }; |