New effects, refactor

This commit is contained in:
Dan 2026-01-09 13:20:06 +00:00
parent 4ac45367e5
commit c0d6bee9c3
14 changed files with 1620 additions and 215 deletions

View file

@ -6,6 +6,120 @@ 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);
}
});
}
/**
@ -133,19 +247,19 @@ export class UIBuilder {
switch (type) {
case 'checkbox':
return this.createCheckbox(id, label, defaultValue, showWhen);
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);
return this.createColor(id, label, defaultValue, showWhen, description);
case 'select':
return this.createSelect(id, label, defaultValue, options, showWhen);
return this.createSelect(id, label, defaultValue, options, showWhen, description);
case 'text':
return this.createTextInput(id, label, defaultValue);
return this.createTextInput(id, label, defaultValue, showWhen, description);
default:
console.warn(`Unknown control type: ${type}`);
@ -156,7 +270,7 @@ export class UIBuilder {
/**
* Create a checkbox control
*/
createCheckbox(id, label, defaultValue, showWhen) {
createCheckbox(id, label, defaultValue, showWhen, description) {
const wrapper = document.createElement('label');
wrapper.className = 'checkbox-label';
@ -176,6 +290,9 @@ export class UIBuilder {
wrapper.dataset.showWhen = showWhen;
}
// Add tooltip handlers to the label wrapper
this.addTooltipHandlers(wrapper, description);
return wrapper;
}
@ -188,9 +305,6 @@ export class UIBuilder {
const labelEl = document.createElement('label');
labelEl.htmlFor = id;
labelEl.innerHTML = `${label}: <span id="${id}-value">${defaultValue}</span>`;
if (description) {
labelEl.title = description;
}
const input = document.createElement('input');
input.type = 'range';
@ -218,13 +332,16 @@ export class UIBuilder {
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) {
createColor(id, label, defaultValue, showWhen, description) {
const container = document.createElement('div');
const labelEl = document.createElement('label');
@ -244,13 +361,16 @@ export class UIBuilder {
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) {
createSelect(id, label, defaultValue, options, showWhen, description) {
const container = document.createElement('div');
const labelEl = document.createElement('label');
@ -278,13 +398,16 @@ export class UIBuilder {
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) {
createTextInput(id, label, defaultValue, showWhen, description) {
const container = document.createElement('div');
const labelEl = document.createElement('label');
@ -300,6 +423,14 @@ export class UIBuilder {
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;
}
@ -348,6 +479,14 @@ export class UIBuilder {
} 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';