`; document.getElementById('embedCodeHost').value = ``; embedModal.style.display = 'flex'; } catch (err) { console.error('embed build failed', err); toast('Could not build the embed — see console'); } embedBtn.textContent = label; embedBtn.disabled = false; } embedBtn.addEventListener('click', openEmbedModal); document.getElementById('embedClose').addEventListener('click', () => embedModal.style.display = 'none'); embedModal.addEventListener('click', e => { if (e.target === embedModal) embedModal.style.display = 'none'; }); document.querySelectorAll('.embedtabs button').forEach(b => b.addEventListener('click', () => { document.querySelectorAll('.embedtabs button').forEach(x => x.classList.toggle('on', x === b)); document.getElementById('embpane-self').style.display = b.dataset.embtab === 'self' ? '' : 'none'; document.getElementById('embpane-host').style.display = b.dataset.embtab === 'host' ? '' : 'none'; })); function copyFrom(taId, btn){ const ta = document.getElementById(taId); const done = () => { const t = btn.textContent; btn.textContent = 'Copied ✓'; setTimeout(() => btn.textContent = t, 1500); }; navigator.clipboard.writeText(ta.value).then(done).catch(() => { ta.focus(); ta.select(); document.execCommand('copy'); done(); }); } document.getElementById('copySelf').addEventListener('click', e => copyFrom('embedCodeSelf', e.currentTarget)); document.getElementById('copyHost').addEventListener('click', e => copyFrom('embedCodeHost', e.currentTarget)); document.getElementById('embedDownload').addEventListener('click', () => { const blob = new Blob([window.__embedFileHtml || ''], { type: 'text/html' }); const a = document.createElement('a'); a.download = 'xaira-watercolor.html'; a.href = URL.createObjectURL(blob); a.click(); setTimeout(() => URL.revokeObjectURL(a.href), 5000); }); /* ---------------- font loading ---------------- */ const fontStatus = document.getElementById('fontStatus'); async function checkDuplet(){ try { await Promise.all(['300', '400', '600'].map(w => document.fonts.load(`${w} 100px Duplet`))); } catch (e) {} await document.fonts.ready; /* canvas can still reach system-activated fonts document.fonts can't see; compare metrics against sans-serif to detect */ const c = document.createElement('canvas').getContext('2d'); c.font = '600 100px sans-serif'; const wFallback = c.measureText('Xiara waters').width; c.font = '600 100px "Duplet", sans-serif'; const wDuplet = c.measureText('Xiara waters').width; const ok = Math.abs(wDuplet - wFallback) > 0.5 || document.fonts.check('600 100px Duplet'); fontStatus.textContent = ok ? '✓ Duplet active' : '⚠ Duplet not found — drop font files to load'; fontStatus.className = ok ? 'ok' : 'warn'; /* fit the subtitle so it spans exactly the logo's width */ const sub = document.querySelector('.brandsub'); const logo = document.getElementById('brandLogo'); if (sub && logo) { const m = document.createElement('canvas').getContext('2d'); m.font = '400 100px "Duplet", sans-serif'; const w100 = m.measureText(sub.textContent).width; const target = logo.getBoundingClientRect().width; if (w100 > 0 && target > 0) sub.style.fontSize = (100 * target / w100).toFixed(2) + 'px'; } drawText(); } /* ---------------- artwork source: Text | SVG ---------------- */ const sourceEl = document.getElementById('source'); function setSource(v){ S.source = v; /* Guard every panel lookup: in an embed the panel is removed, and setSource is still called (by bootLogo) to restore the baked source + render. */ if (sourceEl) sourceEl.querySelectorAll('button').forEach(x => x.classList.toggle('on', x.dataset.source === v)); const show = (id, on) => { const el = document.getElementById(id); if (el) el.style.display = on ? '' : 'none'; }; show('textInput', v === 'text'); show('svgInput', v === 'svg'); show('textControls', v === 'text'); show('svgControls', v === 'svg'); drawText(); } sourceEl.addEventListener('click', e => { const b = e.target.closest('button'); if (!b) return; setSource(b.dataset.source); }); const svgFileEl = document.getElementById('svgFile'); async function loadSVG(file){ const url = URL.createObjectURL(file); try { const img = new Image(); await new Promise((res, rej) => { img.onload = res; img.onerror = rej; img.src = url; }); S.svgImg = img; document.getElementById('svgStatus').textContent = '✓ ' + file.name + ' — click above to replace'; setSource('svg'); /* loading (incl. drag-drop) switches straight to the SVG */ } catch (e) { toast('Could not read that SVG'); } setTimeout(() => URL.revokeObjectURL(url), 2000); } document.getElementById('svgBtn').addEventListener('click', () => svgFileEl.click()); svgFileEl.addEventListener('change', () => { if (svgFileEl.files[0]) loadSVG(svgFileEl.files[0]); svgFileEl.value = ''; }); /* drag & drop font files */ const dropOverlay = document.getElementById('dropOverlay'); let dragDepth = 0; window.addEventListener('dragenter', e => { e.preventDefault(); dragDepth++; dropOverlay.classList.add('show'); }); window.addEventListener('dragleave', e => { e.preventDefault(); if (--dragDepth <= 0) { dragDepth = 0; dropOverlay.classList.remove('show'); } }); window.addEventListener('dragover', e => e.preventDefault()); window.addEventListener('drop', async e => { e.preventDefault(); dragDepth = 0; dropOverlay.classList.remove('show'); for (const f of e.dataTransfer.files) { if (/\.svg$/i.test(f.name)) { loadSVG(f); continue; } if (!/\.(otf|ttf|woff2?)$/i.test(f.name)) continue; const name = f.name.replace(/\.[^.]+$/, ''); try { const face = new FontFace(name, await f.arrayBuffer()); await face.load(); document.fonts.add(face); const opt = document.createElement('option'); opt.value = `400 "${name}"`; opt.textContent = name; opt.selected = true; ctl.weight.appendChild(opt); } catch (err) { console.warn('font load failed', f.name, err); } } drawText(); checkDuplet(); }); /* ---------------- boot ---------------- */ window.__wc = { S, ctl, drawText, applyConfig }; /* console/debug access to live state */ resize(); restoreSettings(); checkDuplet(); /* built-in default artwork: the Xaira logo (from the panel header SVG). Fresh visitors open with the logo in Interactive; returning visitors and embeds keep their own state. */ (async function bootLogo(){ try { const svgText = document.getElementById('brandLogo').outerHTML; const img = new Image(); await new Promise((res, rej) => { img.onload = res; img.onerror = rej; img.src = 'data:image/svg+xml;base64,' + btoa(svgText); }); S.svgImg = img; const st = document.getElementById('svgStatus'); /* absent in embed (panel removed) */ if (st) st.textContent = '✓ Xaira logo (built-in) — click above to replace'; let savedSource = null; if (window.__EMBED__) savedSource = window.__EMBED__.source || null; else { try { const s = localStorage.getItem(SAVE_KEY); if (s) savedSource = JSON.parse(s).source || 'text'; } catch (e) {} } if (savedSource === 'svg') setSource('svg'); /* restore now that the logo exists */ else if (savedSource == null && !window.__EMBED__) { /* first-ever open: logo + interactive */ setSource('svg'); document.querySelector('#modes button[data-mode="2"]').click(); } } catch (e) {} })(); /* embed mode: apply the baked config and remove the whole control UI */ if (window.__EMBED__) { applyConfig(window.__EMBED__); /* also starts the over-time cycle timer if that mode was exported */ panel.remove(); const tg = document.getElementById('panelToggle'); if (tg) tg.remove(); } requestAnimationFrame(frame); " style="display:block;width:100%;height:360px;border:0" title="Xaira watercolor">