balloonDesign/script.js

697 lines
31 KiB
JavaScript

// script.js - shared tab/UI logic
(() => {
'use strict';
document.addEventListener('DOMContentLoaded', () => {
const ACTIVE_TAB_KEY = 'balloonDesigner:activeTab:v1';
// Ensure shared helpers are ready
if (!window.shared) return;
const { clamp, clamp01, SWATCH_TEXTURE_ZOOM } = window.shared;
const { FLAT_COLORS } = window.shared;
// Modal helpers
const messageModal = document.getElementById('message-modal');
const modalText = document.getElementById('modal-text');
const modalCloseBtn = document.getElementById('modal-close-btn');
function showModal(msg) {
if (!messageModal || !modalText) return;
modalText.textContent = msg;
messageModal.classList.remove('hidden');
}
function hideModal() {
messageModal?.classList.add('hidden');
}
modalCloseBtn?.addEventListener('click', hideModal);
// Tab elements
const orgSheet = document.getElementById('controls-panel');
const claSheet = document.getElementById('classic-controls-panel');
const wallSheet = document.getElementById('wall-controls-panel');
const orgSection = document.getElementById('tab-organic');
const claSection = document.getElementById('tab-classic');
const wallSection = document.getElementById('tab-wall');
const tabBtns = Array.from(document.querySelectorAll('#mode-tabs .tab-btn'));
const mobileActionBar = document.getElementById('mobile-action-bar');
// Export buttons
const exportPngBtn = document.querySelector('[data-export="png"]');
const exportSvgBtn = document.querySelector('[data-export="svg"]');
const { XLINK_NS } = window.shared || {};
const MOBILE_TAB_DEFAULT = 'controls';
const MOBILE_TAB_PREFIX = 'designer:mobileTab:';
const exportModal = document.getElementById('export-modal');
const exportModalClose = document.getElementById('export-modal-close');
// Generic color picker modal (used by replace chips)
(function initColorPickerModal() {
const modal = document.getElementById('color-picker-modal');
const grid = document.getElementById('color-picker-grid');
const titleEl = document.getElementById('color-picker-title');
const subtitleEl = document.getElementById('color-picker-subtitle');
const closeBtn = document.getElementById('color-picker-close');
if (!modal || !grid || !titleEl || !subtitleEl) return;
const close = () => modal.classList.add('hidden');
closeBtn?.addEventListener('click', close);
modal.addEventListener('click', (e) => { if (e.target === modal) close(); });
const setChipStyle = (el, meta) => {
if (!el || !meta) return;
if (meta.image) {
const zoom = Math.max(1, meta.imageZoom ?? SWATCH_TEXTURE_ZOOM ?? 2.5);
el.style.backgroundImage = `url("${meta.image}")`;
el.style.backgroundColor = meta.hex || '#fff';
el.style.backgroundSize = `${100 * zoom}%`;
el.style.backgroundPosition = `${(meta.imageFocus?.x ?? 0.5) * 100}% ${(meta.imageFocus?.y ?? 0.5) * 100}%`;
} else {
el.style.backgroundImage = 'none';
el.style.backgroundColor = meta.hex || '#fff';
}
};
window.openColorPicker = ({ title = 'Choose a color', subtitle = '', items = [], onSelect }) => {
titleEl.textContent = title;
subtitleEl.textContent = subtitle;
grid.innerHTML = '';
items.forEach(item => {
const meta = item.meta || (Number.isInteger(item.idx) ? FLAT_COLORS?.[item.idx] : null) || {};
const sw = document.createElement('button');
sw.type = 'button';
sw.className = 'color-option';
sw.setAttribute('aria-label', item.label || meta.name || meta.hex || 'Color');
const swatch = document.createElement('div');
swatch.className = 'swatch';
setChipStyle(swatch, meta.hex ? meta : { hex: item.hex });
const label = document.createElement('div');
label.className = 'label';
label.textContent = item.label || meta.name || meta.hex || 'Color';
const metaLine = document.createElement('div');
metaLine.className = 'meta';
metaLine.textContent = item.metaText || meta.hex || '';
sw.appendChild(swatch);
sw.appendChild(label);
sw.appendChild(metaLine);
sw.addEventListener('click', () => {
close();
onSelect?.(item);
});
grid.appendChild(sw);
});
modal.classList.remove('hidden');
};
})();
function getImageHref(el) {
return el.getAttribute('href') || (XLINK_NS ? el.getAttributeNS(XLINK_NS, 'href') : null);
}
function setImageHref(el, val) {
el.setAttribute('href', val);
if (XLINK_NS) el.setAttributeNS(XLINK_NS, 'xlink:href', val);
}
// Update export buttons visibility depending on tab/design
window.updateExportButtonVisibility = () => {
const tab = detectCurrentTab();
const isWall = tab === '#tab-wall';
const isClassic = tab === '#tab-classic';
const hasClassic = !!document.querySelector('#classic-display svg');
const hasWall = !!document.querySelector('#wall-display svg');
const enable = (btn, on) => { if (btn) btn.disabled = !on; };
if (isClassic) enable(exportPngBtn, hasClassic); else enable(exportPngBtn, true);
if (isClassic) enable(exportSvgBtn, hasClassic); else if (isWall) enable(exportSvgBtn, hasWall); else enable(exportSvgBtn, true);
};
async function buildClassicSvgExportFromDom() {
const svgElement = document.querySelector('#classic-display svg');
if (!svgElement) throw new Error('Classic design not found. Please create a design first.');
const clonedSvg = svgElement.cloneNode(true);
const allImages = Array.from(clonedSvg.querySelectorAll('image'));
await Promise.all(allImages.map(async img => {
const href = getImageHref(img);
if (!href || href.startsWith('data:')) return;
const dataUrl = await window.shared.imageUrlToDataUrl(href);
if (dataUrl) setImageHref(img, dataUrl);
}));
if (!clonedSvg.getAttribute('xmlns')) clonedSvg.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
if (!clonedSvg.getAttribute('xmlns:xlink') && XLINK_NS) clonedSvg.setAttribute('xmlns:xlink', XLINK_NS);
let width = Number(clonedSvg.getAttribute('width')) || null;
let height = Number(clonedSvg.getAttribute('height')) || null;
const viewBox = (clonedSvg.getAttribute('viewBox') || '').split(/\s+/).map(Number);
if ((!width || !height) && viewBox.length === 4 && viewBox.every(v => Number.isFinite(v))) {
width = width || viewBox[2];
height = height || viewBox[3];
}
if (!width || !height) {
const vbW = svgElement.clientWidth || 1000;
const vbH = svgElement.clientHeight || 1000;
width = width || vbW;
height = height || vbH;
}
clonedSvg.setAttribute('width', width);
clonedSvg.setAttribute('height', height);
return { svgString: new XMLSerializer().serializeToString(clonedSvg), width, height };
}
async function buildWallSvgExportFromDom() {
const svgElement = document.querySelector('#wall-display svg');
if (!svgElement) throw new Error('Wall design not found. Please create a design first.');
const clonedSvg = svgElement.cloneNode(true);
const allImages = Array.from(clonedSvg.querySelectorAll('image'));
await Promise.all(allImages.map(async img => {
const href = getImageHref(img);
if (!href || href.startsWith('data:')) return;
const dataUrl = await window.shared.imageUrlToDataUrl(href);
if (dataUrl) setImageHref(img, dataUrl);
}));
if (!clonedSvg.getAttribute('xmlns')) clonedSvg.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
if (!clonedSvg.getAttribute('xmlns:xlink') && XLINK_NS) clonedSvg.setAttribute('xmlns:xlink', XLINK_NS);
let width = Number(clonedSvg.getAttribute('width')) || null;
let height = Number(clonedSvg.getAttribute('height')) || null;
const viewBox = (clonedSvg.getAttribute('viewBox') || '').split(/\s+/).map(Number);
if ((!width || !height) && viewBox.length === 4 && viewBox.every(v => Number.isFinite(v))) {
width = width || viewBox[2];
height = height || viewBox[3];
}
if (!width || !height) {
const vbW = svgElement.clientWidth || 1000;
const vbH = svgElement.clientHeight || 1000;
width = width || vbW;
height = height || vbH;
}
clonedSvg.setAttribute('width', width);
clonedSvg.setAttribute('height', height);
return { svgString: new XMLSerializer().serializeToString(clonedSvg), width, height };
}
// Export routing
async function buildWallSvgExportFromDom() {
const svgElement = document.querySelector('#wall-display svg');
if (!svgElement) throw new Error('Wall design not found. Please create a design first.');
const clonedSvg = svgElement.cloneNode(true);
const allImages = Array.from(clonedSvg.querySelectorAll('image'));
await Promise.all(allImages.map(async img => {
const href = getImageHref(img);
if (!href || href.startsWith('data:')) return;
const dataUrl = await window.shared.imageUrlToDataUrl(href);
if (dataUrl) setImageHref(img, dataUrl);
}));
if (!clonedSvg.getAttribute('xmlns')) clonedSvg.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
if (!clonedSvg.getAttribute('xmlns:xlink') && XLINK_NS) clonedSvg.setAttribute('xmlns:xlink', XLINK_NS);
let width = Number(clonedSvg.getAttribute('width')) || null;
let height = Number(clonedSvg.getAttribute('height')) || null;
const viewBox = (clonedSvg.getAttribute('viewBox') || '').split(/\s+/).map(Number);
if ((!width || !height) && viewBox.length === 4 && viewBox.every(v => Number.isFinite(v))) {
width = width || viewBox[2];
height = height || viewBox[3];
}
if (!width || !height) {
const vbW = svgElement.clientWidth || 1000;
const vbH = svgElement.clientHeight || 1000;
width = width || vbW;
height = height || vbH;
}
clonedSvg.setAttribute('width', width);
clonedSvg.setAttribute('height', height);
const svgString = new XMLSerializer().serializeToString(clonedSvg);
return { svgString, width, height };
}
async function inlineSvgImages(svgString) {
try {
const parser = new DOMParser();
const doc = parser.parseFromString(svgString, 'image/svg+xml');
if (doc.querySelector('parsererror')) return svgString;
const imgs = Array.from(doc.querySelectorAll('image'));
await Promise.all(imgs.map(async img => {
const href = img.getAttribute('href') || img.getAttribute('xlink:href');
if (!href || href.startsWith('data:')) return;
const dataUrl = await window.shared.imageUrlToDataUrl(href);
if (dataUrl) {
img.setAttribute('href', dataUrl);
img.removeAttribute('xlink:href');
}
}));
return new XMLSerializer().serializeToString(doc.documentElement);
} catch (err) {
console.warn('[Export] inlineSvgImages failed', err);
return svgString;
}
}
async function svgStringToPng(svgString, width, height) {
const { PNG_EXPORT_SCALE } = window.shared;
if (!/xmlns=/.test(svgString)) {
svgString = svgString.replace('<svg', '<svg xmlns="http://www.w3.org/2000/svg"');
}
// Inline any external images to avoid CORS/fetch issues.
svgString = await inlineSvgImages(svgString);
const img = new Image();
img.crossOrigin = 'anonymous';
const scale = PNG_EXPORT_SCALE;
const canvasEl = document.createElement('canvas');
canvasEl.width = Math.max(1, Math.round(width * scale));
canvasEl.height = Math.max(1, Math.round(height * scale));
const ctx2 = canvasEl.getContext('2d');
if (ctx2) {
ctx2.imageSmoothingEnabled = true;
ctx2.imageSmoothingQuality = 'high';
}
const loadSrc = (src) => new Promise((resolve, reject) => {
img.onload = resolve;
img.onerror = () => reject(new Error('Could not rasterize SVG.'));
img.src = src;
});
// Prefer base64 data URL to avoid parser issues with raw text.
const svg64 = btoa(unescape(encodeURIComponent(svgString)));
const dataUrl = `data:image/svg+xml;base64,${svg64}`;
try {
await loadSrc(dataUrl);
} catch (e) {
// Fallback to blob if base64 path fails.
let blobUrl = null;
try {
const blob = new Blob([svgString], { type: 'image/svg+xml;charset=utf-8' });
blobUrl = URL.createObjectURL(blob);
await loadSrc(blobUrl);
} finally {
if (blobUrl) URL.revokeObjectURL(blobUrl);
}
}
ctx2.drawImage(img, 0, 0, canvasEl.width, canvasEl.height);
return canvasEl.toDataURL('image/png');
}
function downloadSvg(svgString, filename) {
const { download } = window.shared;
const blob = new Blob([svgString], { type: 'image/svg+xml;charset=utf-8' });
const url = URL.createObjectURL(blob);
download(url, filename);
setTimeout(() => URL.revokeObjectURL(url), 20000);
}
async function exportPng() {
try {
const tab = detectCurrentTab();
if (tab === '#tab-classic') {
const { svgString, width, height } = window.ClassicExport?.buildClassicSvgPayload
? await window.ClassicExport.buildClassicSvgPayload()
: await buildClassicSvgExportFromDom();
const pngUrl = await svgStringToPng(svgString, width, height);
window.shared.download(pngUrl, 'classic_design.png');
return;
}
if (tab === '#tab-wall') {
const { svgString, width, height } = await buildWallSvgExportFromDom();
const pngUrl = await svgStringToPng(svgString, width, height);
window.shared.download(pngUrl, 'wall_design.png');
return;
}
const { svgString, width, height } = await window.organic.buildOrganicSvgPayload();
const pngUrl = await svgStringToPng(svgString, width, height);
window.shared.download(pngUrl, 'balloon_design.png');
} catch (err) {
console.error('[Export PNG] Failed:', err);
showModal(err.message || 'Could not export PNG. Check console for details.');
}
}
async function exportSvg() {
try {
const tab = detectCurrentTab();
if (tab === '#tab-classic') {
const { svgString, width, height } = window.ClassicExport?.buildClassicSvgPayload
? await window.ClassicExport.buildClassicSvgPayload()
: await buildClassicSvgExportFromDom();
try {
const pngUrl = await svgStringToPng(svgString, width, height);
const cleanSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">
<image href="${pngUrl}" x="0" y="0" width="${width}" height="${height}" preserveAspectRatio="xMidYMid meet" />
</svg>`;
downloadSvg(cleanSvg, 'classic_design.svg');
} catch (pngErr) {
console.warn('[Export SVG] PNG embed failed, falling back to vector-only SVG', pngErr);
downloadSvg(svgString, 'classic_design.svg');
}
return;
}
if (tab === '#tab-wall') {
const { svgString, width, height } = await buildWallSvgExportFromDom();
downloadSvg(svgString, 'wall_design.svg');
return;
}
const { svgString } = await window.organic.buildOrganicSvgPayload();
downloadSvg(svgString, 'organic_design.svg');
} catch (err) {
console.error('[Export] Failed:', err);
showModal(err.message || 'Could not export. Check console for details.');
}
}
const openExportModal = () => { exportModal?.classList.remove('hidden'); };
const closeExportModal = () => { exportModal?.classList.add('hidden'); };
exportModalClose?.addEventListener('click', closeExportModal);
exportModal?.addEventListener('click', (e) => { if (e.target === exportModal) closeExportModal(); });
document.body.addEventListener('click', e => {
const choice = e.target.closest('[data-export-choice]');
if (choice) {
const type = choice.dataset.exportChoice;
closeExportModal();
if (type === 'png') exportPng();
else if (type === 'svg') exportSvg();
return;
}
const btn = e.target.closest('[data-export]');
if (!btn) return;
e.preventDefault();
openExportModal();
});
// Tab logic
function detectCurrentTab() {
const bodyActive = document.body?.dataset?.activeTab;
const activeBtn = document.querySelector('#mode-tabs .tab-btn.tab-active');
const classicVisible = !document.getElementById('tab-classic')?.classList.contains('hidden');
const organicVisible = !document.getElementById('tab-organic')?.classList.contains('hidden');
const wallVisible = !document.getElementById('tab-wall')?.classList.contains('hidden');
let id = bodyActive || activeBtn?.dataset?.target;
if (!id) {
if (classicVisible && !organicVisible && !wallVisible) id = '#tab-classic';
else if (organicVisible && !classicVisible && !wallVisible) id = '#tab-organic';
else if (wallVisible && !classicVisible && !organicVisible) id = '#tab-wall';
}
if (!id) id = '#tab-organic';
if (document.body) document.body.dataset.activeTab = id;
return id;
}
window.detectCurrentTab = detectCurrentTab;
function updateSheets() {
const tab = detectCurrentTab();
const hide = !window.matchMedia('(min-width: 1024px)').matches && document.body?.dataset?.controlsHidden === '1';
if (orgSheet) orgSheet.classList.toggle('hidden', hide || tab !== '#tab-organic');
if (claSheet) claSheet.classList.toggle('hidden', hide || tab !== '#tab-classic');
if (wallSheet) wallSheet.classList.toggle('hidden', hide || tab !== '#tab-wall');
}
function updateMobileStacks(tabName) {
const orgPanel = document.getElementById('controls-panel');
const claPanel = document.getElementById('classic-controls-panel');
const wallPanel = document.getElementById('wall-controls-panel');
const currentTab = detectCurrentTab();
const panel = currentTab === '#tab-classic' ? claPanel : (currentTab === '#tab-wall' ? wallPanel : orgPanel);
const target = tabName || document.body?.dataset?.mobileTab || MOBILE_TAB_DEFAULT;
const isHidden = document.body?.dataset?.controlsHidden === '1';
const isDesktop = window.matchMedia('(min-width: 1024px)').matches;
if (!panel) return;
const stacks = Array.from(panel.querySelectorAll('.control-stack'));
if (!stacks.length) return;
stacks.forEach(stack => {
if (isHidden) {
stack.style.display = 'none';
} else if (isDesktop) {
stack.style.display = '';
} else {
const show = stack.dataset.mobileTab === target;
stack.style.display = show ? 'block' : 'none';
}
});
}
function setMobileTab(tab, mainTabId, skipPersist = false) {
const name = tab || MOBILE_TAB_DEFAULT;
const activeMainTab = mainTabId || detectCurrentTab();
if (!skipPersist && activeMainTab) {
try { localStorage.setItem(`${MOBILE_TAB_PREFIX}${activeMainTab}`, name); } catch {}
}
const isDesktop = window.matchMedia('(min-width: 1024px)').matches;
if (document.body) {
document.body.dataset.mobileTab = name;
delete document.body.dataset.controlsHidden;
}
// Ensure the current panel is not minimized/hidden when we select a tab.
const panel = activeMainTab === '#tab-classic'
? document.getElementById('classic-controls-panel')
: (activeMainTab === '#tab-wall'
? document.getElementById('wall-controls-panel')
: document.getElementById('controls-panel'));
panel?.classList.remove('minimized');
if (panel) panel.style.display = '';
updateSheets();
updateMobileStacks(name);
const buttons = document.querySelectorAll('#mobile-tabbar .mobile-tab-btn');
buttons.forEach(btn => btn.setAttribute('aria-pressed', String(btn.dataset.mobileTab === name)));
}
window.__setMobileTab = setMobileTab;
const NUDGE_POS_KEY = 'classic:nudgePos:v1';
const NUDGE_MARGIN = 12;
const NUDGE_SIZE_HINT = { w: 180, h: 200 };
let floatingNudgeCollapsed = false;
function clampNudgePos(pos, el) {
const vw = window.innerWidth || 1024;
const vh = window.innerHeight || 768;
const rect = el?.getBoundingClientRect();
const w = rect?.width || NUDGE_SIZE_HINT.w;
const h = rect?.height || NUDGE_SIZE_HINT.h;
return {
x: Math.min(Math.max(pos.x, NUDGE_MARGIN), Math.max(NUDGE_MARGIN, vw - w - NUDGE_MARGIN)),
y: Math.min(Math.max(pos.y, NUDGE_MARGIN), Math.max(NUDGE_MARGIN, vh - h - NUDGE_MARGIN))
};
}
let nudgePos = null;
let nudgePosInitialized = false;
function loadNudgePos(el) {
try {
const saved = JSON.parse(localStorage.getItem(NUDGE_POS_KEY));
if (saved && typeof saved.x === 'number' && typeof saved.y === 'number') return clampNudgePos(saved, el);
} catch {}
return clampNudgePos({ x: (window.innerWidth || 1024) - 240, y: 120 }, el);
}
function ensureNudgePos(el) {
if (!nudgePos) nudgePos = loadNudgePos(el);
return clampNudgePos(nudgePos, el);
}
function saveNudgePos(pos, el) {
nudgePos = clampNudgePos(pos, el);
try { localStorage.setItem(NUDGE_POS_KEY, JSON.stringify(nudgePos)); } catch {}
return nudgePos;
}
function applyNudgePos(el, pos) {
const p = clampNudgePos(pos || ensureNudgePos(el), el);
el.style.left = `${p.x}px`;
el.style.top = `${p.y}px`;
el.style.right = 'auto';
el.style.bottom = 'auto';
nudgePos = p;
nudgePosInitialized = true;
}
function updateFloatingNudge() {
const el = document.getElementById('floating-topper-nudge');
if (!el) return;
const classicActive = document.body?.dataset.activeTab === '#tab-classic';
const topperActive = document.body?.dataset.topperOverlay === '1';
const shouldShow = classicActive && topperActive;
const shouldShowPanel = shouldShow && !floatingNudgeCollapsed;
el.classList.toggle('hidden', !shouldShowPanel);
el.classList.toggle('collapsed', floatingNudgeCollapsed);
el.style.display = shouldShowPanel ? 'block' : 'none';
if (shouldShowPanel && !nudgePosInitialized) applyNudgePos(el, ensureNudgePos(el));
}
function showFloatingNudge() { floatingNudgeCollapsed = false; updateFloatingNudge(); }
function hideFloatingNudge() {
floatingNudgeCollapsed = true;
const nudge = document.getElementById('floating-topper-nudge');
if (nudge) { nudge.classList.add('hidden'); nudge.style.display = 'none'; }
const tab = document.getElementById('floating-nudge-tab');
if (tab) tab.classList.remove('hidden');
updateFloatingNudge();
}
window.__updateFloatingNudge = updateFloatingNudge;
window.__showFloatingNudge = showFloatingNudge;
window.__hideFloatingNudge = hideFloatingNudge;
// Tab switching
if (orgSection && claSection && tabBtns.length > 0) {
let current = '#tab-organic';
const isMobileView = () => window.matchMedia('(max-width: 1023px)').matches;
const updateMobileActionBarVisibility = () => {
if (!mobileActionBar) return;
const modalOpen = !!document.querySelector('.color-modal:not(.hidden)');
const shouldShow = current === '#tab-organic' && isMobileView() && !modalOpen;
mobileActionBar.classList.toggle('hidden', !shouldShow);
};
const wireMobileActionButtons = () => {
const guardOrganic = () => current === '#tab-organic';
const clickBtn = (sel) => { if (!guardOrganic()) return; document.querySelector(sel)?.click(); };
const on = (id, fn) => document.getElementById(id)?.addEventListener('click', fn);
on('mobile-act-undo', () => clickBtn('#tool-undo'));
on('mobile-act-redo', () => clickBtn('#tool-redo'));
on('mobile-act-eyedrop', () => clickBtn('#tool-eyedropper'));
on('mobile-act-erase', () => {
if (!guardOrganic()) return;
const erase = document.getElementById('tool-erase');
const draw = document.getElementById('tool-draw');
const active = erase?.getAttribute('aria-pressed') === 'true';
(active ? draw : erase)?.click();
});
on('mobile-act-clear', () => clickBtn('#clear-canvas-btn-top'));
on('mobile-act-export', () => clickBtn('[data-export="png"]'));
};
wireMobileActionButtons();
window.addEventListener('resize', updateMobileActionBarVisibility);
function setTab(id, isInitial = false) {
if (!id || !document.querySelector(id)) id = '#tab-organic';
current = id;
if (document.body) document.body.dataset.activeTab = id;
orgSheet?.classList.remove('minimized');
claSheet?.classList.remove('minimized');
wallSheet?.classList.remove('minimized');
orgSection.classList.toggle('hidden', id !== '#tab-organic');
claSection.classList.toggle('hidden', id !== '#tab-classic');
wallSection?.classList.toggle('hidden', id !== '#tab-wall');
updateSheets();
updateFloatingNudge();
tabBtns.forEach(btn => {
const active = btn.dataset.target === id;
btn.classList.toggle('tab-active', active);
btn.classList.toggle('tab-idle', !active);
btn.setAttribute('aria-pressed', String(active));
});
if (!isInitial) {
try { localStorage.setItem(ACTIVE_TAB_KEY, id); } catch {}
}
if (document.body) delete document.body.dataset.controlsHidden;
const isOrganic = id === '#tab-organic';
const showHeaderColor = id !== '#tab-classic';
const clearTop = document.getElementById('clear-canvas-btn-top');
if (clearTop) {
clearTop.classList.toggle('hidden', !isOrganic);
clearTop.style.display = isOrganic ? '' : 'none';
}
const headerActiveSwatch = document.getElementById('current-color-chip-global')?.closest('.flex');
headerActiveSwatch?.classList.toggle('hidden', !showHeaderColor);
const savedMobile = (() => {
try { return localStorage.getItem(`${MOBILE_TAB_PREFIX}${id}`) || MOBILE_TAB_DEFAULT; } catch {}
return MOBILE_TAB_DEFAULT;
})();
if (document.body) document.body.dataset.mobileTab = savedMobile;
setMobileTab(savedMobile, id, true);
orgSheet?.classList.toggle('hidden', id !== '#tab-organic');
claSheet?.classList.toggle('hidden', id !== '#tab-classic');
wallSheet?.classList.toggle('hidden', id !== '#tab-wall');
window.updateExportButtonVisibility();
updateMobileActionBarVisibility();
}
tabBtns.forEach(btn => {
btn.addEventListener('click', (e) => {
const button = e.target.closest('button[data-target]');
if (button) setTab(button.dataset.target);
});
});
let savedTab = null;
try { savedTab = localStorage.getItem(ACTIVE_TAB_KEY); } catch {}
setTab(savedTab || '#tab-organic', true);
window.__whichTab = () => current;
if (!document.body?.dataset?.mobileTab) {
const initialMobile = (() => {
try { return localStorage.getItem(`${MOBILE_TAB_PREFIX}${current}`) || MOBILE_TAB_DEFAULT; } catch {}
return MOBILE_TAB_DEFAULT;
})();
document.body.dataset.mobileTab = initialMobile;
}
setMobileTab(document.body.dataset.mobileTab, current, true);
updateSheets();
updateMobileStacks(document.body.dataset.mobileTab);
updateMobileActionBarVisibility();
}
// Mobile tabbar
(function initMobileTabs() {
const buttons = Array.from(document.querySelectorAll('#mobile-tabbar .mobile-tab-btn'));
if (!buttons.length) return;
buttons.forEach(btn => {
btn.addEventListener('click', () => {
const tab = btn.dataset.mobileTab || 'controls';
const activeTabId = detectCurrentTab();
const panel = activeTabId === '#tab-classic'
? document.getElementById('classic-controls-panel')
: (activeTabId === '#tab-wall'
? document.getElementById('wall-controls-panel')
: document.getElementById('controls-panel'));
const currentTab = document.body.dataset.mobileTab;
if (tab === currentTab) {
panel.classList.toggle('minimized');
} else {
panel.classList.remove('minimized');
setMobileTab(tab);
panel.scrollTop = 0;
}
});
});
const mq = window.matchMedia('(min-width: 1024px)');
const sync = () => {
const current = document.body?.dataset?.mobileTab || MOBILE_TAB_DEFAULT;
setMobileTab(current, detectCurrentTab(), true);
updateFloatingNudge();
};
mq.addEventListener('change', sync);
setMobileTab(document.body?.dataset?.mobileTab || MOBILE_TAB_DEFAULT, detectCurrentTab(), true);
sync();
const nudgeToggle = document.getElementById('floating-nudge-toggle');
nudgeToggle?.addEventListener('click', () => {
hideFloatingNudge();
});
const nudge = document.getElementById('floating-topper-nudge');
const nudgeHeader = document.querySelector('#floating-topper-nudge .floating-nudge-header');
if (nudge && nudgeHeader) {
let dragId = null;
let start = null;
nudgeHeader.addEventListener('pointerdown', (e) => {
if (e.target.closest('#floating-nudge-toggle')) return;
dragId = e.pointerId;
start = { x: e.clientX, y: e.clientY, pos: loadNudgePos(nudge) };
nudge.classList.add('dragging');
nudgeHeader.setPointerCapture(dragId);
});
nudgeHeader.addEventListener('pointermove', (e) => {
if (dragId === null || e.pointerId !== dragId) return;
if (!start) return;
const dx = e.clientX - start.x;
const dy = e.clientY - start.y;
const next = clampNudgePos({ x: start.pos.x + dx, y: start.pos.y + dy });
applyNudgePos(nudge, next);
});
const endDrag = (e) => {
if (dragId === null || e.pointerId !== dragId) return;
const rect = nudge.getBoundingClientRect();
const next = clampNudgePos({ x: rect.left, y: rect.top }, nudge);
saveNudgePos(next, nudge);
nudge.classList.remove('dragging');
dragId = null;
start = null;
try { nudgeHeader.releasePointerCapture(e.pointerId); } catch {}
};
nudgeHeader.addEventListener('pointerup', endDrag);
nudgeHeader.addEventListener('pointercancel', endDrag);
window.addEventListener('resize', () => {
const pos = clampNudgePos(loadNudgePos(nudge), nudge);
saveNudgePos(pos, nudge);
nudgePosInitialized = false;
applyNudgePos(nudge, pos);
});
}
})();
});
})();