/** * UI Builder - Dynamically generates control UI from effect definitions */ export class UIBuilder { constructor(containerElement) { this.container = containerElement; this.controlGroups = new Map(); // category -> { element, controls } this.tooltip = null; this.tooltipTimeout = null; this.setupTooltip(); } /** * Create and setup the tooltip element */ setupTooltip() { // Wait for DOM to be ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { this.createTooltipElement(); }); } else { this.createTooltipElement(); } } createTooltipElement() { this.tooltip = document.createElement('div'); this.tooltip.className = 'control-tooltip'; this.tooltip.style.cssText = ` position: fixed; background: linear-gradient(135deg, rgba(0, 120, 200, 0.98) 0%, rgba(0, 100, 180, 0.98) 100%); color: #fff; padding: 0.5rem 0.75rem; border-radius: 4px; font-size: 0.8rem; pointer-events: none; z-index: 10000; max-width: 250px; box-shadow: 0 0 20px rgba(0, 150, 255, 0.5), 0 4px 12px rgba(0, 0, 0, 0.4); border: 1px solid rgba(0, 150, 255, 0.6); opacity: 0; transition: opacity 0.15s ease; line-height: 1.4; text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); `; document.body.appendChild(this.tooltip); } /** * Show tooltip for an element */ showTooltip(element, text) { if (!text || !this.tooltip) return; clearTimeout(this.tooltipTimeout); this.tooltip.textContent = text; this.tooltip.style.opacity = '1'; // Position tooltip above the element const rect = element.getBoundingClientRect(); // Set initial position to measure this.tooltip.style.left = '0px'; this.tooltip.style.top = '0px'; this.tooltip.style.visibility = 'hidden'; this.tooltip.style.display = 'block'; const tooltipRect = this.tooltip.getBoundingClientRect(); this.tooltip.style.visibility = 'visible'; let left = rect.left + rect.width / 2 - tooltipRect.width / 2; let top = rect.top - tooltipRect.height - 10; // Keep tooltip on screen const padding = 10; if (left < padding) left = padding; if (left + tooltipRect.width > window.innerWidth - padding) { left = window.innerWidth - tooltipRect.width - padding; } if (top < padding) { top = rect.bottom + 10; } this.tooltip.style.left = `${left}px`; this.tooltip.style.top = `${top}px`; } /** * Hide tooltip */ hideTooltip() { if (!this.tooltip) return; clearTimeout(this.tooltipTimeout); this.tooltipTimeout = setTimeout(() => { this.tooltip.style.opacity = '0'; }, 100); } /** * Add tooltip handlers to an element */ addTooltipHandlers(element, description) { if (!description) return; element.addEventListener('mouseenter', () => { this.showTooltip(element, description); }); element.addEventListener('mouseleave', () => { this.hideTooltip(); }); element.addEventListener('mousemove', () => { // Update position on mouse move for better following if (this.tooltip && this.tooltip.style.opacity === '1') { this.showTooltip(element, description); } }); } /** * Build the entire UI from registered effects * @param {Array} effects - All registered effects */ buildUI(effects) { // Clear existing content this.container.innerHTML = ''; this.controlGroups.clear(); // Group effects by category const categorized = this.categorizeEffects(effects); // Create control groups for each category for (const [category, categoryEffects] of categorized) { this.createControlGroup(category, categoryEffects); } } /** * Categorize effects by their category property * @param {Array} effects * @returns {Map>} */ categorizeEffects(effects) { const categories = new Map(); effects.forEach(effect => { const category = effect.category || 'Other'; if (!categories.has(category)) { categories.set(category, []); } categories.get(category).push(effect); }); // Sort categories in a logical order const orderedCategories = new Map(); const categoryOrder = [ 'Text Line 1', 'Text Line 2', 'Background', 'Background Animations', 'Border', 'Visual Effects', 'General Effects', 'Special Effects' ]; categoryOrder.forEach(cat => { if (categories.has(cat)) { orderedCategories.set(cat, categories.get(cat)); } }); // Add any remaining categories categories.forEach((effects, cat) => { if (!orderedCategories.has(cat)) { orderedCategories.set(cat, effects); } }); return orderedCategories; } /** * Create a collapsible control group * @param {string} category - Category name * @param {Array} effects - Effects in this category */ createControlGroup(category, effects) { const groupDiv = document.createElement('div'); groupDiv.className = 'control-group'; // Create header const header = document.createElement('h3'); header.className = 'control-group-header'; header.innerHTML = ` ${category} `; // Create content container const content = document.createElement('div'); content.className = 'control-group-content'; // Add controls for each effect in this category effects.forEach(effect => { this.addEffectControls(content, effect); }); // Add click handler for collapsing header.addEventListener('click', () => { groupDiv.classList.toggle('collapsed'); }); groupDiv.appendChild(header); groupDiv.appendChild(content); this.container.appendChild(groupDiv); this.controlGroups.set(category, { element: groupDiv, effects }); } /** * Add controls for a single effect * @param {HTMLElement} container - Container to add controls to * @param {ButtonEffect} effect - Effect to create controls for */ addEffectControls(container, effect) { effect.controls.forEach(control => { const controlEl = this.createControl(control); if (controlEl) { container.appendChild(controlEl); } }); } /** * Create a single control element * @param {Object} controlDef - Control definition from effect * @returns {HTMLElement} */ createControl(controlDef) { const { id, type, label, defaultValue, min, max, step, options, showWhen, description } = controlDef; switch (type) { case 'checkbox': return this.createCheckbox(id, label, defaultValue, showWhen, description); case 'range': return this.createRange(id, label, defaultValue, min, max, step, description, showWhen); case 'color': return this.createColor(id, label, defaultValue, showWhen, description); case 'select': return this.createSelect(id, label, defaultValue, options, showWhen, description); case 'text': return this.createTextInput(id, label, defaultValue, showWhen, description); default: console.warn(`Unknown control type: ${type}`); return null; } } /** * Create a checkbox control */ createCheckbox(id, label, defaultValue, showWhen, description) { const wrapper = document.createElement('label'); wrapper.className = 'checkbox-label'; const input = document.createElement('input'); input.type = 'checkbox'; input.id = id; input.checked = defaultValue || false; const span = document.createElement('span'); span.textContent = label; wrapper.appendChild(input); wrapper.appendChild(span); if (showWhen) { wrapper.style.display = 'none'; wrapper.dataset.showWhen = showWhen; } // Add tooltip handlers to the label wrapper this.addTooltipHandlers(wrapper, description); return wrapper; } /** * Create a range slider control */ createRange(id, label, defaultValue, min, max, step, description, showWhen) { const container = document.createElement('div'); const labelEl = document.createElement('label'); labelEl.htmlFor = id; labelEl.innerHTML = `${label}: ${defaultValue}`; const input = document.createElement('input'); input.type = 'range'; input.id = id; input.min = min !== undefined ? min : 0; input.max = max !== undefined ? max : 100; input.value = defaultValue !== undefined ? defaultValue : 50; if (step !== undefined) { input.step = step; } // Update value display on input input.addEventListener('input', () => { const valueDisplay = document.getElementById(`${id}-value`); if (valueDisplay) { valueDisplay.textContent = input.value; } }); container.appendChild(labelEl); container.appendChild(input); if (showWhen) { container.style.display = 'none'; container.dataset.showWhen = showWhen; } // Add tooltip handlers to the label this.addTooltipHandlers(labelEl, description); return container; } /** * Create a color picker control */ createColor(id, label, defaultValue, showWhen, description) { const container = document.createElement('div'); const labelEl = document.createElement('label'); labelEl.htmlFor = id; labelEl.textContent = label; const input = document.createElement('input'); input.type = 'color'; input.id = id; input.value = defaultValue || '#ffffff'; container.appendChild(labelEl); container.appendChild(input); if (showWhen) { container.style.display = 'none'; container.dataset.showWhen = showWhen; } // Add tooltip handlers to the label this.addTooltipHandlers(labelEl, description); return container; } /** * Create a select dropdown control */ createSelect(id, label, defaultValue, options, showWhen, description) { const container = document.createElement('div'); const labelEl = document.createElement('label'); labelEl.htmlFor = id; labelEl.textContent = label; const select = document.createElement('select'); select.id = id; options.forEach(opt => { const option = document.createElement('option'); option.value = opt.value; option.textContent = opt.label; if (opt.value === defaultValue) { option.selected = true; } select.appendChild(option); }); container.appendChild(labelEl); container.appendChild(select); if (showWhen) { container.style.display = 'none'; container.dataset.showWhen = showWhen; } // Add tooltip handlers to the label this.addTooltipHandlers(labelEl, description); return container; } /** * Create a text input control */ createTextInput(id, label, defaultValue, showWhen, description) { const container = document.createElement('div'); const labelEl = document.createElement('label'); labelEl.htmlFor = id; labelEl.textContent = label; const input = document.createElement('input'); input.type = 'text'; input.id = id; input.value = defaultValue || ''; input.maxLength = 20; container.appendChild(labelEl); container.appendChild(input); if (showWhen) { container.style.display = 'none'; container.dataset.showWhen = showWhen; } // Add tooltip handlers to the label this.addTooltipHandlers(labelEl, description); return container; } /** * Setup conditional visibility for controls * Should be called after all controls are created */ setupConditionalVisibility() { // Find all controls with showWhen attribute const conditionalControls = this.container.querySelectorAll('[data-show-when]'); conditionalControls.forEach(control => { const triggerControlId = control.dataset.showWhen; const triggerControl = document.getElementById(triggerControlId); if (triggerControl) { const updateVisibility = () => { if (triggerControl.type === 'checkbox') { control.style.display = triggerControl.checked ? 'block' : 'none'; } else if (triggerControl.tagName === 'SELECT') { // Get the control ID to determine what value to check for const controlId = control.querySelector('input, select')?.id; // For background controls if (triggerControlId === 'bg-type') { if (controlId === 'bg-color') { control.style.display = triggerControl.value === 'solid' ? 'block' : 'none'; } else if (controlId && (controlId.startsWith('gradient-') || controlId === 'gradient-angle')) { control.style.display = triggerControl.value === 'gradient' ? 'block' : 'none'; } else if (controlId && (controlId.startsWith('texture-') || controlId === 'texture-type' || controlId === 'texture-scale')) { control.style.display = triggerControl.value === 'texture' ? 'block' : 'none'; } else if (controlId && (controlId.startsWith('emoji-') || controlId === 'emoji-text')) { control.style.display = triggerControl.value === 'emoji-wallpaper' ? 'block' : 'none'; } } // For text color controls else if (triggerControlId === 'text-color-type') { if (controlId === 'text-color') { control.style.display = triggerControl.value === 'solid' ? 'block' : 'none'; } else if (controlId && controlId.startsWith('text-gradient-')) { control.style.display = triggerControl.value === 'gradient' ? 'block' : 'none'; } } else if (triggerControlId === 'text2-color-type') { if (controlId === 'text2-color') { control.style.display = triggerControl.value === 'solid' ? 'block' : 'none'; } else if (controlId && controlId.startsWith('text2-gradient-')) { control.style.display = triggerControl.value === 'gradient' ? 'block' : 'none'; } } // For border style controls else if (triggerControlId === 'border-style') { if (controlId === 'border-rainbow-speed') { control.style.display = triggerControl.value === 'rainbow' ? 'block' : 'none'; } else if (controlId === 'border-march-speed') { control.style.display = triggerControl.value === 'marching-ants' ? 'block' : 'none'; } } else { // Default: show when any value is selected control.style.display = triggerControl.value ? 'block' : 'none'; } } }; // Initial visibility updateVisibility(); // Update on change triggerControl.addEventListener('change', updateVisibility); triggerControl.addEventListener('input', updateVisibility); } }); } }