Loads of button generator additions
This commit is contained in:
parent
98b4edf47d
commit
e7754141bf
12 changed files with 904 additions and 126 deletions
|
|
@ -174,6 +174,14 @@ export class ButtonGenerator {
|
||||||
values[id] = element.value;
|
values[id] = element.value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// For range-dual controls, also check for -start and -end suffixed elements
|
||||||
|
const startElement = document.getElementById(`${id}-start`);
|
||||||
|
const endElement = document.getElementById(`${id}-end`);
|
||||||
|
if (startElement && endElement) {
|
||||||
|
values[`${id}-start`] = parseFloat(startElement.value);
|
||||||
|
values[`${id}-end`] = parseFloat(endElement.value);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return values;
|
return values;
|
||||||
|
|
|
||||||
|
|
@ -245,12 +245,17 @@ export class ExternalImageBackgroundEffect extends ButtonEffect {
|
||||||
|
|
||||||
apply(context, controlValues, animState, renderData) {
|
apply(context, controlValues, animState, renderData) {
|
||||||
const file = controlValues['bg-image-file'];
|
const file = controlValues['bg-image-file'];
|
||||||
|
const bgColor = controlValues['bg-color'] || '#FFFFFF';
|
||||||
const fitMode = controlValues['bg-image-fit'] || 'cover';
|
const fitMode = controlValues['bg-image-fit'] || 'cover';
|
||||||
const opacity = controlValues['bg-image-opacity'] ?? 1;
|
const opacity = controlValues['bg-image-opacity'] ?? 1;
|
||||||
const zoom = controlValues['bg-image-zoom'] ?? 100;
|
const zoom = controlValues['bg-image-zoom'] ?? 100;
|
||||||
const offsetX = controlValues['bg-image-offset-x'] ?? 50;
|
const offsetX = controlValues['bg-image-offset-x'] ?? 50;
|
||||||
const offsetY = controlValues['bg-image-offset-y'] ?? 50;
|
const offsetY = controlValues['bg-image-offset-y'] ?? 50;
|
||||||
|
|
||||||
|
// Draw background color first (always, for all states)
|
||||||
|
context.fillStyle = bgColor;
|
||||||
|
context.fillRect(0, 0, renderData.width, renderData.height);
|
||||||
|
|
||||||
// If no file selected, fill with a placeholder color
|
// If no file selected, fill with a placeholder color
|
||||||
if (!file || !file.blobUrl) {
|
if (!file || !file.blobUrl) {
|
||||||
context.fillStyle = '#cccccc';
|
context.fillStyle = '#cccccc';
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ export class SolidBackgroundEffect extends ButtonEffect {
|
||||||
label: "Background Color",
|
label: "Background Color",
|
||||||
defaultValue: "#4a90e2",
|
defaultValue: "#4a90e2",
|
||||||
showWhen: "bg-type",
|
showWhen: "bg-type",
|
||||||
description: "Solid background color",
|
description: "Background color (also used behind images)",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
88
static/js/button-generator/effects/flash-text.js
Normal file
88
static/js/button-generator/effects/flash-text.js
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
import { ButtonEffect } from '../effect-base.js';
|
||||||
|
|
||||||
|
export class FlashTextEffect extends ButtonEffect {
|
||||||
|
constructor(textLineNumber = 1) {
|
||||||
|
const suffix = textLineNumber === 1 ? '' : '2';
|
||||||
|
super({
|
||||||
|
id: `text-flash${suffix}`,
|
||||||
|
name: `Flash Text ${textLineNumber}`,
|
||||||
|
type: textLineNumber === 1 ? 'text' : 'text2',
|
||||||
|
category: textLineNumber === 1 ? 'Text Line 1' : 'Text Line 2',
|
||||||
|
renderOrder: 1, // Execute very early, before all other text effects
|
||||||
|
textLineNumber: textLineNumber
|
||||||
|
});
|
||||||
|
this.textLineNumber = textLineNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
defineControls() {
|
||||||
|
const textLineNumber = this.textLineNumber || this.config?.textLineNumber || 1;
|
||||||
|
const suffix = textLineNumber === 1 ? '' : '2';
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: `animate-text-flash${suffix}`,
|
||||||
|
type: 'checkbox',
|
||||||
|
label: 'Flash Visibility',
|
||||||
|
defaultValue: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: `flash-range${suffix}`,
|
||||||
|
type: 'range-dual',
|
||||||
|
label: 'Visible Frame Range',
|
||||||
|
defaultValueStart: textLineNumber === 1 ? 0 : 20,
|
||||||
|
defaultValueEnd: textLineNumber === 1 ? 19 : 39,
|
||||||
|
min: 0,
|
||||||
|
max: 39,
|
||||||
|
step: 1,
|
||||||
|
showWhen: `animate-text-flash${suffix}`,
|
||||||
|
description: 'Frame range where text is visible (0-39 frames total)'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
isEnabled(controlValues) {
|
||||||
|
const suffix = this.textLineNumber === 1 ? '' : '2';
|
||||||
|
return controlValues[`animate-text-flash${suffix}`] === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
apply(context, controlValues, animState, renderData) {
|
||||||
|
if (!animState) return; // Flash requires animation
|
||||||
|
|
||||||
|
const suffix = this.textLineNumber === 1 ? '' : '2';
|
||||||
|
const startFrame = controlValues[`flash-range${suffix}-start`] || 0;
|
||||||
|
const endFrame = controlValues[`flash-range${suffix}-end`] || 39;
|
||||||
|
|
||||||
|
// Check if current frame is within visible range
|
||||||
|
const isVisible = this.isFrameVisible(animState.frame, startFrame, endFrame);
|
||||||
|
|
||||||
|
// Store visibility state in renderData so other text effects can check it
|
||||||
|
const visibilityKey = `textFlashVisible${suffix}`;
|
||||||
|
renderData[visibilityKey] = isVisible;
|
||||||
|
|
||||||
|
// Also set globalAlpha (even though it gets restored, it helps during this effect's lifecycle)
|
||||||
|
if (!isVisible) {
|
||||||
|
context.globalAlpha = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if frame is within visible range
|
||||||
|
* @param {number} frame - Current frame number (0-39)
|
||||||
|
* @param {number} startFrame - Start of visible range
|
||||||
|
* @param {number} endFrame - End of visible range
|
||||||
|
* @returns {boolean} - True if frame is in visible range
|
||||||
|
*/
|
||||||
|
isFrameVisible(frame, startFrame, endFrame) {
|
||||||
|
// Ensure start is less than or equal to end
|
||||||
|
const start = Math.min(startFrame, endFrame);
|
||||||
|
const end = Math.max(startFrame, endFrame);
|
||||||
|
|
||||||
|
return frame >= start && frame <= end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-register effects for both text lines
|
||||||
|
export function register(generator) {
|
||||||
|
generator.registerEffect(new FlashTextEffect(1));
|
||||||
|
generator.registerEffect(new FlashTextEffect(2));
|
||||||
|
}
|
||||||
|
|
@ -52,6 +52,18 @@ export class RainbowTextEffect extends ButtonEffect {
|
||||||
|
|
||||||
const suffix = this.textLineNumber === 1 ? '' : '2';
|
const suffix = this.textLineNumber === 1 ? '' : '2';
|
||||||
|
|
||||||
|
// Check if ticker is active - if so, ticker handles rendering
|
||||||
|
const tickerActive = controlValues[`animate-text-ticker${suffix}`];
|
||||||
|
if (tickerActive) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check flash visibility - if flash is active and text is invisible, don't render
|
||||||
|
const flashActive = controlValues[`animate-text-flash${suffix}`];
|
||||||
|
if (flashActive && renderData[`textFlashVisible${suffix}`] === false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Get text configuration
|
// Get text configuration
|
||||||
const text = controlValues[`button-text${suffix}`] || '';
|
const text = controlValues[`button-text${suffix}`] || '';
|
||||||
if (!text || text.trim() === '') return;
|
if (!text || text.trim() === '') return;
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,19 @@ export class SpinTextEffect extends ButtonEffect {
|
||||||
|
|
||||||
apply(context, controlValues, animState, renderData) {
|
apply(context, controlValues, animState, renderData) {
|
||||||
const suffix = this.textLineNumber === 1 ? "" : "2";
|
const suffix = this.textLineNumber === 1 ? "" : "2";
|
||||||
|
|
||||||
|
// Check if ticker is active - if so, ticker handles rendering
|
||||||
|
const tickerActive = controlValues[`animate-text-ticker${suffix}`];
|
||||||
|
if (tickerActive) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check flash visibility - if flash is active and text is invisible, don't render
|
||||||
|
const flashActive = controlValues[`animate-text-flash${suffix}`];
|
||||||
|
if (flashActive && renderData[`textFlashVisible${suffix}`] === false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const text = controlValues[`button-text${suffix}`] || "";
|
const text = controlValues[`button-text${suffix}`] || "";
|
||||||
|
|
||||||
if (!text || text.trim() === '') return;
|
if (!text || text.trim() === '') return;
|
||||||
|
|
|
||||||
|
|
@ -98,6 +98,12 @@ export class TextShadowEffect extends ButtonEffect {
|
||||||
apply(context, controlValues, animState, renderData) {
|
apply(context, controlValues, animState, renderData) {
|
||||||
const suffix = this.textLineNumber === 1 ? "" : "2";
|
const suffix = this.textLineNumber === 1 ? "" : "2";
|
||||||
|
|
||||||
|
// Check flash visibility - if flash is active and text is invisible, don't render
|
||||||
|
const flashActive = controlValues[`animate-text-flash${suffix}`];
|
||||||
|
if (flashActive && renderData[`textFlashVisible${suffix}`] === false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const text = controlValues[`button-text${suffix}`] || "";
|
const text = controlValues[`button-text${suffix}`] || "";
|
||||||
if (!text) return;
|
if (!text) return;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -150,13 +150,20 @@ export class StandardTextEffect extends ButtonEffect {
|
||||||
const waveActive = controlValues[`animate-text-wave${suffix}`];
|
const waveActive = controlValues[`animate-text-wave${suffix}`];
|
||||||
const rainbowActive = controlValues[`animate-text-rainbow${suffix}`];
|
const rainbowActive = controlValues[`animate-text-rainbow${suffix}`];
|
||||||
const spinActive = controlValues[`animate-text-spin${suffix}`];
|
const spinActive = controlValues[`animate-text-spin${suffix}`];
|
||||||
|
const tickerActive = controlValues[`animate-text-ticker${suffix}`];
|
||||||
|
|
||||||
return text && text.trim() !== "" && !waveActive && !rainbowActive && !spinActive;
|
return text && text.trim() !== "" && !waveActive && !rainbowActive && !spinActive && !tickerActive;
|
||||||
}
|
}
|
||||||
|
|
||||||
apply(context, controlValues, animState, renderData) {
|
apply(context, controlValues, animState, renderData) {
|
||||||
const suffix = this.textLineNumber === 1 ? "" : "2";
|
const suffix = this.textLineNumber === 1 ? "" : "2";
|
||||||
|
|
||||||
|
// Check flash visibility - if flash is active and text is invisible, don't render
|
||||||
|
const flashActive = controlValues[`animate-text-flash${suffix}`];
|
||||||
|
if (flashActive && renderData[`textFlashVisible${suffix}`] === false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const text = controlValues[`button-text${suffix}`];
|
const text = controlValues[`button-text${suffix}`];
|
||||||
if (!text) return;
|
if (!text) return;
|
||||||
|
|
||||||
|
|
|
||||||
312
static/js/button-generator/effects/ticker-text.js
Normal file
312
static/js/button-generator/effects/ticker-text.js
Normal file
|
|
@ -0,0 +1,312 @@
|
||||||
|
import { ButtonEffect } from '../effect-base.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ticker text animation effect
|
||||||
|
* Makes text scroll across the button in various directions with seamless looping
|
||||||
|
*/
|
||||||
|
export class TickerTextEffect extends ButtonEffect {
|
||||||
|
constructor(textLineNumber = 1) {
|
||||||
|
const suffix = textLineNumber === 1 ? '' : '2';
|
||||||
|
super({
|
||||||
|
id: `text-ticker${suffix}`,
|
||||||
|
name: `Ticker Text ${textLineNumber}`,
|
||||||
|
type: textLineNumber === 1 ? 'text' : 'text2',
|
||||||
|
category: textLineNumber === 1 ? 'Text Line 1' : 'Text Line 2',
|
||||||
|
renderOrder: 12, // After wave(10) and spin(8), before shadow(19) and standard(20)
|
||||||
|
textLineNumber: textLineNumber
|
||||||
|
});
|
||||||
|
this.textLineNumber = textLineNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
defineControls() {
|
||||||
|
const textLineNumber = this.textLineNumber || this.config?.textLineNumber || 1;
|
||||||
|
const suffix = textLineNumber === 1 ? '' : '2';
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: `animate-text-ticker${suffix}`,
|
||||||
|
type: 'checkbox',
|
||||||
|
label: 'Ticker Scroll',
|
||||||
|
defaultValue: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: `ticker-direction${suffix}`,
|
||||||
|
type: 'select',
|
||||||
|
label: 'Scroll Direction',
|
||||||
|
defaultValue: 'left',
|
||||||
|
options: [
|
||||||
|
{ value: 'left', label: 'Right to Left' },
|
||||||
|
{ value: 'right', label: 'Left to Right' },
|
||||||
|
{ value: 'up', label: 'Down to Up' },
|
||||||
|
{ value: 'down', label: 'Up to Down' }
|
||||||
|
],
|
||||||
|
showWhen: `animate-text-ticker${suffix}`,
|
||||||
|
description: 'Direction of text scrolling'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
isEnabled(controlValues) {
|
||||||
|
const suffix = this.textLineNumber === 1 ? '' : '2';
|
||||||
|
return controlValues[`animate-text-ticker${suffix}`] === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
apply(context, controlValues, animState, renderData) {
|
||||||
|
if (!animState) return; // Ticker requires animation
|
||||||
|
|
||||||
|
const suffix = this.textLineNumber === 1 ? '' : '2';
|
||||||
|
|
||||||
|
// Check flash visibility - if flash is active and text is invisible, don't render
|
||||||
|
const flashActive = controlValues[`animate-text-flash${suffix}`];
|
||||||
|
if (flashActive && renderData[`textFlashVisible${suffix}`] === false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get text configuration
|
||||||
|
const text = controlValues[`button-text${suffix}`] || '';
|
||||||
|
if (!text || text.trim() === '') return;
|
||||||
|
|
||||||
|
const fontSize = controlValues[`font-size${suffix}`] || 12;
|
||||||
|
const fontWeight = controlValues[`font-bold${suffix}`] ? 'bold' : 'normal';
|
||||||
|
const fontStyle = controlValues[`font-italic${suffix}`] ? 'italic' : 'normal';
|
||||||
|
const fontFamily = controlValues[`font-family${suffix}`] || 'Arial';
|
||||||
|
|
||||||
|
const baseX = (controlValues[`text${suffix}-x`] / 100) * renderData.width;
|
||||||
|
const baseY = (controlValues[`text${suffix}-y`] / 100) * renderData.height;
|
||||||
|
|
||||||
|
const direction = controlValues[`ticker-direction${suffix}`] || 'left';
|
||||||
|
|
||||||
|
// Set font
|
||||||
|
context.font = `${fontStyle} ${fontWeight} ${fontSize}px "${fontFamily}"`;
|
||||||
|
context.textAlign = 'center';
|
||||||
|
context.textBaseline = 'middle';
|
||||||
|
|
||||||
|
// Check if other effects are active
|
||||||
|
const waveActive = controlValues[`animate-text-wave${suffix}`];
|
||||||
|
const spinActive = controlValues[`animate-text-spin${suffix}`];
|
||||||
|
const rainbowActive = controlValues[`animate-text-rainbow${suffix}`];
|
||||||
|
|
||||||
|
// Split text into grapheme clusters (handles emojis properly)
|
||||||
|
const chars = this.splitGraphemes(text);
|
||||||
|
|
||||||
|
// Measure total width
|
||||||
|
const totalWidth = context.measureText(text).width;
|
||||||
|
|
||||||
|
// Calculate scroll parameters - SIMPLIFIED
|
||||||
|
const horizontal = direction === 'left' || direction === 'right';
|
||||||
|
const gapSize = 50; // Gap between text repetitions
|
||||||
|
|
||||||
|
// For ticker to work: text must scroll across full screen width PLUS its own width PLUS gap
|
||||||
|
// This ensures text fully enters, crosses, and exits with proper spacing
|
||||||
|
const copySpacing = horizontal
|
||||||
|
? (renderData.width + totalWidth + gapSize)
|
||||||
|
: (renderData.height + fontSize * 2 + gapSize);
|
||||||
|
|
||||||
|
// For a seamless loop, offset scrolls through one full copy spacing in 40 frames
|
||||||
|
// At frame 0: offset = 0
|
||||||
|
// At frame 39: offset approaches copySpacing (ready to wrap to next copy)
|
||||||
|
const offset = animState.progress * copySpacing;
|
||||||
|
|
||||||
|
// Apply direction
|
||||||
|
const scrollOffset = {
|
||||||
|
x: direction === 'left' ? -offset : (direction === 'right' ? offset : 0),
|
||||||
|
y: direction === 'up' ? -offset : (direction === 'down' ? offset : 0)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Calculate how many copies we need to fill the screen
|
||||||
|
const numCopies = horizontal
|
||||||
|
? Math.ceil(renderData.width / copySpacing) + 3
|
||||||
|
: Math.ceil(renderData.height / copySpacing) + 3;
|
||||||
|
|
||||||
|
// Get colors
|
||||||
|
const colors = this.getTextColors(context, controlValues, text, baseX, baseY, fontSize, animState, rainbowActive);
|
||||||
|
|
||||||
|
// Set ticker active flag so other effects can skip rendering if needed
|
||||||
|
renderData[`tickerActive${suffix}`] = true;
|
||||||
|
|
||||||
|
// Render scrolling text
|
||||||
|
this.renderScrollingText(
|
||||||
|
context, chars, scrollOffset, numCopies,
|
||||||
|
totalWidth, fontSize, copySpacing, horizontal, direction,
|
||||||
|
{ wave: waveActive, spin: spinActive, rainbow: rainbowActive },
|
||||||
|
controlValues, animState, renderData, colors
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Split text into grapheme clusters (emoji-safe)
|
||||||
|
*/
|
||||||
|
splitGraphemes(text) {
|
||||||
|
if (typeof Intl !== 'undefined' && Intl.Segmenter) {
|
||||||
|
const segmenter = new Intl.Segmenter('en', { granularity: 'grapheme' });
|
||||||
|
return Array.from(segmenter.segment(text), s => s.segment);
|
||||||
|
} else {
|
||||||
|
// Fallback: spread operator handles basic emoji
|
||||||
|
return [...text];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render scrolling text with multiple copies for seamless looping
|
||||||
|
*/
|
||||||
|
renderScrollingText(
|
||||||
|
context, chars, scrollOffset, numCopies,
|
||||||
|
totalWidth, fontSize, copySpacing, horizontal, direction,
|
||||||
|
effects, controlValues, animState, renderData, colors
|
||||||
|
) {
|
||||||
|
const suffix = this.textLineNumber === 1 ? '' : '2';
|
||||||
|
|
||||||
|
// Get wave parameters if active
|
||||||
|
let waveAmplitude, waveSpeed;
|
||||||
|
if (effects.wave) {
|
||||||
|
waveAmplitude = controlValues[`wave-amplitude${suffix}`] || 3;
|
||||||
|
waveSpeed = controlValues[`wave-speed${suffix}`] || 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get spin parameters if active
|
||||||
|
let spinSpeed, spinStagger;
|
||||||
|
if (effects.spin) {
|
||||||
|
spinSpeed = controlValues[`spin-speed${suffix}`] || 1;
|
||||||
|
spinStagger = controlValues[`spin-stagger${suffix}`] || 0.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get base positioning from controls
|
||||||
|
const baseX = (controlValues[`text${suffix}-x`] / 100) * renderData.width;
|
||||||
|
const baseY = (controlValues[`text${suffix}-y`] / 100) * renderData.height;
|
||||||
|
|
||||||
|
// Loop through copies - render multiple instances for seamless wrap
|
||||||
|
for (let copy = 0; copy < numCopies; copy++) {
|
||||||
|
// Each copy is spaced by copySpacing (which includes text width + gap)
|
||||||
|
const copyOffsetX = horizontal ? copy * copySpacing : 0;
|
||||||
|
const copyOffsetY = !horizontal ? copy * copySpacing : 0;
|
||||||
|
|
||||||
|
// Calculate starting position based on direction
|
||||||
|
// The key: text should be fully OFF screen before appearing on the other side
|
||||||
|
let startX, startY;
|
||||||
|
|
||||||
|
if (direction === 'left') {
|
||||||
|
// Right to left: Position so after scrolling copySpacing left, text fully exits
|
||||||
|
// copySpacing = totalWidth + gap
|
||||||
|
// Start with left edge at: copySpacing (so right edge is at copySpacing + totalWidth)
|
||||||
|
// After scrolling copySpacing left: right edge is at totalWidth (still need to exit more!)
|
||||||
|
// Actually: start at renderData.width so left edge begins at right screen edge
|
||||||
|
startX = renderData.width;
|
||||||
|
} else if (direction === 'right') {
|
||||||
|
// Left to right: Start with RIGHT edge of text at left edge of screen
|
||||||
|
// Text scrolls right, exits when LEFT edge reaches right edge of screen
|
||||||
|
startX = -totalWidth;
|
||||||
|
} else if (direction === 'up') {
|
||||||
|
// Down to up: Start off-screen below
|
||||||
|
startX = baseX - totalWidth / 2; // Center the text horizontally
|
||||||
|
startY = renderData.height;
|
||||||
|
} else { // down
|
||||||
|
// Up to down: Start off-screen above
|
||||||
|
startX = baseX - totalWidth / 2; // Center the text horizontally
|
||||||
|
startY = -fontSize * 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate current position with scroll offset and copy offset
|
||||||
|
let currentX = startX + scrollOffset.x + copyOffsetX;
|
||||||
|
let currentY = horizontal ? baseY : (startY + scrollOffset.y + copyOffsetY);
|
||||||
|
|
||||||
|
// Render each character
|
||||||
|
for (let i = 0; i < chars.length; i++) {
|
||||||
|
const char = chars[i];
|
||||||
|
const charWidth = context.measureText(char).width;
|
||||||
|
const charCenterX = currentX + charWidth / 2;
|
||||||
|
let charY = currentY;
|
||||||
|
|
||||||
|
// Apply wave effect if active
|
||||||
|
let waveY = 0;
|
||||||
|
if (effects.wave) {
|
||||||
|
const phase = animState.getPhase(waveSpeed);
|
||||||
|
const charOffset = i / chars.length;
|
||||||
|
waveY = Math.sin(phase + charOffset * Math.PI * 2) * waveAmplitude;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply spin effect if active
|
||||||
|
if (effects.spin) {
|
||||||
|
const phase = animState.getPhase(spinSpeed);
|
||||||
|
const rotation = (phase + i * spinStagger * Math.PI * 2) % (Math.PI * 2);
|
||||||
|
|
||||||
|
context.save();
|
||||||
|
context.translate(charCenterX, charY + waveY);
|
||||||
|
context.rotate(rotation);
|
||||||
|
|
||||||
|
// Draw outline if enabled
|
||||||
|
if (controlValues[`text${suffix}-outline`]) {
|
||||||
|
context.strokeStyle = colors.strokeStyle;
|
||||||
|
context.lineWidth = 2;
|
||||||
|
context.strokeText(char, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw character
|
||||||
|
context.fillStyle = colors.fillStyle;
|
||||||
|
context.fillText(char, 0, 0);
|
||||||
|
context.restore();
|
||||||
|
} else {
|
||||||
|
// No spin - draw normally with wave offset
|
||||||
|
const finalY = charY + waveY;
|
||||||
|
|
||||||
|
// Draw outline if enabled
|
||||||
|
if (controlValues[`text${suffix}-outline`]) {
|
||||||
|
context.strokeStyle = colors.strokeStyle;
|
||||||
|
context.lineWidth = 2;
|
||||||
|
context.strokeText(char, charCenterX, finalY);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw character
|
||||||
|
context.fillStyle = colors.fillStyle;
|
||||||
|
context.fillText(char, charCenterX, finalY);
|
||||||
|
}
|
||||||
|
|
||||||
|
currentX += charWidth;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get text colors (solid, gradient, or rainbow)
|
||||||
|
*/
|
||||||
|
getTextColors(context, controlValues, text, x, y, fontSize, animState, rainbowActive) {
|
||||||
|
const suffix = this.textLineNumber === 1 ? '' : '2';
|
||||||
|
|
||||||
|
let fillStyle, strokeStyle;
|
||||||
|
|
||||||
|
// Check if rainbow text is also enabled
|
||||||
|
if (animState && rainbowActive) {
|
||||||
|
const speed = controlValues[`text-rainbow-speed${suffix}`] || 1;
|
||||||
|
const hue = (animState.progress * speed * 360) % 360;
|
||||||
|
fillStyle = `hsl(${hue}, 80%, 60%)`;
|
||||||
|
strokeStyle = `hsl(${hue}, 80%, 30%)`;
|
||||||
|
} else {
|
||||||
|
const colorType = controlValues[`text${suffix}-color-type`] || 'solid';
|
||||||
|
|
||||||
|
if (colorType === 'solid') {
|
||||||
|
fillStyle = controlValues[`text${suffix}-color`] || '#ffffff';
|
||||||
|
strokeStyle = controlValues[`outline${suffix}-color`] || '#000000';
|
||||||
|
} else {
|
||||||
|
// Gradient
|
||||||
|
const angle = (controlValues[`text${suffix}-gradient-angle`] || 0) * (Math.PI / 180);
|
||||||
|
const textWidth = context.measureText(text).width;
|
||||||
|
const x1 = x - textWidth / 2 + (Math.cos(angle) * textWidth) / 2;
|
||||||
|
const y1 = y - fontSize / 2 + (Math.sin(angle) * fontSize) / 2;
|
||||||
|
const x2 = x + textWidth / 2 - (Math.cos(angle) * textWidth) / 2;
|
||||||
|
const y2 = y + fontSize / 2 - (Math.sin(angle) * fontSize) / 2;
|
||||||
|
|
||||||
|
const gradient = context.createLinearGradient(x1, y1, x2, y2);
|
||||||
|
gradient.addColorStop(0, controlValues[`text${suffix}-gradient-color1`] || '#ffffff');
|
||||||
|
gradient.addColorStop(1, controlValues[`text${suffix}-gradient-color2`] || '#00ffff');
|
||||||
|
fillStyle = gradient;
|
||||||
|
strokeStyle = controlValues[`outline${suffix}-color`] || '#000000';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { fillStyle, strokeStyle };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-register effect
|
||||||
|
export function register(generator) {
|
||||||
|
generator.registerEffect(new TickerTextEffect(1));
|
||||||
|
generator.registerEffect(new TickerTextEffect(2));
|
||||||
|
}
|
||||||
|
|
@ -63,6 +63,18 @@ export class WaveTextEffect extends ButtonEffect {
|
||||||
|
|
||||||
const suffix = this.textLineNumber === 1 ? '' : '2';
|
const suffix = this.textLineNumber === 1 ? '' : '2';
|
||||||
|
|
||||||
|
// Check if ticker is active - if so, ticker handles rendering
|
||||||
|
const tickerActive = controlValues[`animate-text-ticker${suffix}`];
|
||||||
|
if (tickerActive) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check flash visibility - if flash is active and text is invisible, don't render
|
||||||
|
const flashActive = controlValues[`animate-text-flash${suffix}`];
|
||||||
|
if (flashActive && renderData[`textFlashVisible${suffix}`] === false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Get text configuration
|
// Get text configuration
|
||||||
const text = controlValues[`button-text${suffix}`] || '';
|
const text = controlValues[`button-text${suffix}`] || '';
|
||||||
if (!text || text.trim() === '') return;
|
if (!text || text.trim() === '') return;
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,9 @@ import * as standardText from "./effects/text-standard.js";
|
||||||
import * as textShadow from "./effects/text-shadow.js";
|
import * as textShadow from "./effects/text-shadow.js";
|
||||||
import * as waveText from "./effects/wave-text.js";
|
import * as waveText from "./effects/wave-text.js";
|
||||||
import * as rainbowText from "./effects/rainbow-text.js";
|
import * as rainbowText from "./effects/rainbow-text.js";
|
||||||
|
import * as flashText from "./effects/flash-text.js";
|
||||||
import * as spinText from "./effects/spin-text.js";
|
import * as spinText from "./effects/spin-text.js";
|
||||||
|
import * as tickerText from "./effects/ticker-text.js";
|
||||||
import * as glitch from "./effects/glitch.js";
|
import * as glitch from "./effects/glitch.js";
|
||||||
import * as pulse from "./effects/pulse.js";
|
import * as pulse from "./effects/pulse.js";
|
||||||
import * as shimmer from "./effects/shimmer.js";
|
import * as shimmer from "./effects/shimmer.js";
|
||||||
|
|
@ -101,7 +103,9 @@ async function setupApp() {
|
||||||
textShadow.register(generator);
|
textShadow.register(generator);
|
||||||
waveText.register(generator);
|
waveText.register(generator);
|
||||||
rainbowText.register(generator);
|
rainbowText.register(generator);
|
||||||
|
flashText.register(generator);
|
||||||
spinText.register(generator);
|
spinText.register(generator);
|
||||||
|
tickerText.register(generator);
|
||||||
glitch.register(generator);
|
glitch.register(generator);
|
||||||
pulse.register(generator);
|
pulse.register(generator);
|
||||||
shimmer.register(generator);
|
shimmer.register(generator);
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,8 @@ export class UIBuilder {
|
||||||
*/
|
*/
|
||||||
setupTooltip() {
|
setupTooltip() {
|
||||||
// Wait for DOM to be ready
|
// Wait for DOM to be ready
|
||||||
if (document.readyState === 'loading') {
|
if (document.readyState === "loading") {
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
this.createTooltipElement();
|
this.createTooltipElement();
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -26,8 +26,8 @@ export class UIBuilder {
|
||||||
}
|
}
|
||||||
|
|
||||||
createTooltipElement() {
|
createTooltipElement() {
|
||||||
this.tooltip = document.createElement('div');
|
this.tooltip = document.createElement("div");
|
||||||
this.tooltip.className = 'control-tooltip';
|
this.tooltip.className = "control-tooltip";
|
||||||
this.tooltip.style.cssText = `
|
this.tooltip.style.cssText = `
|
||||||
position: fixed;
|
position: fixed;
|
||||||
background: linear-gradient(135deg, rgba(0, 120, 200, 0.98) 0%, rgba(0, 100, 180, 0.98) 100%);
|
background: linear-gradient(135deg, rgba(0, 120, 200, 0.98) 0%, rgba(0, 100, 180, 0.98) 100%);
|
||||||
|
|
@ -57,20 +57,20 @@ export class UIBuilder {
|
||||||
clearTimeout(this.tooltipTimeout);
|
clearTimeout(this.tooltipTimeout);
|
||||||
|
|
||||||
this.tooltip.textContent = text;
|
this.tooltip.textContent = text;
|
||||||
this.tooltip.style.opacity = '1';
|
this.tooltip.style.opacity = "1";
|
||||||
|
|
||||||
// Position tooltip above the element
|
// Position tooltip above the element
|
||||||
const rect = element.getBoundingClientRect();
|
const rect = element.getBoundingClientRect();
|
||||||
|
|
||||||
// Set initial position to measure
|
// Set initial position to measure
|
||||||
this.tooltip.style.left = '0px';
|
this.tooltip.style.left = "0px";
|
||||||
this.tooltip.style.top = '0px';
|
this.tooltip.style.top = "0px";
|
||||||
this.tooltip.style.visibility = 'hidden';
|
this.tooltip.style.visibility = "hidden";
|
||||||
this.tooltip.style.display = 'block';
|
this.tooltip.style.display = "block";
|
||||||
|
|
||||||
const tooltipRect = this.tooltip.getBoundingClientRect();
|
const tooltipRect = this.tooltip.getBoundingClientRect();
|
||||||
|
|
||||||
this.tooltip.style.visibility = 'visible';
|
this.tooltip.style.visibility = "visible";
|
||||||
|
|
||||||
let left = rect.left + rect.width / 2 - tooltipRect.width / 2;
|
let left = rect.left + rect.width / 2 - tooltipRect.width / 2;
|
||||||
let top = rect.top - tooltipRect.height - 10;
|
let top = rect.top - tooltipRect.height - 10;
|
||||||
|
|
@ -96,7 +96,7 @@ export class UIBuilder {
|
||||||
if (!this.tooltip) return;
|
if (!this.tooltip) return;
|
||||||
clearTimeout(this.tooltipTimeout);
|
clearTimeout(this.tooltipTimeout);
|
||||||
this.tooltipTimeout = setTimeout(() => {
|
this.tooltipTimeout = setTimeout(() => {
|
||||||
this.tooltip.style.opacity = '0';
|
this.tooltip.style.opacity = "0";
|
||||||
}, 100);
|
}, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -106,17 +106,17 @@ export class UIBuilder {
|
||||||
addTooltipHandlers(element, description) {
|
addTooltipHandlers(element, description) {
|
||||||
if (!description) return;
|
if (!description) return;
|
||||||
|
|
||||||
element.addEventListener('mouseenter', () => {
|
element.addEventListener("mouseenter", () => {
|
||||||
this.showTooltip(element, description);
|
this.showTooltip(element, description);
|
||||||
});
|
});
|
||||||
|
|
||||||
element.addEventListener('mouseleave', () => {
|
element.addEventListener("mouseleave", () => {
|
||||||
this.hideTooltip();
|
this.hideTooltip();
|
||||||
});
|
});
|
||||||
|
|
||||||
element.addEventListener('mousemove', () => {
|
element.addEventListener("mousemove", () => {
|
||||||
// Update position on mouse move for better following
|
// Update position on mouse move for better following
|
||||||
if (this.tooltip && this.tooltip.style.opacity === '1') {
|
if (this.tooltip && this.tooltip.style.opacity === "1") {
|
||||||
this.showTooltip(element, description);
|
this.showTooltip(element, description);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -128,7 +128,7 @@ export class UIBuilder {
|
||||||
*/
|
*/
|
||||||
buildUI(effects) {
|
buildUI(effects) {
|
||||||
// Clear existing content
|
// Clear existing content
|
||||||
this.container.innerHTML = '';
|
this.container.innerHTML = "";
|
||||||
this.controlGroups.clear();
|
this.controlGroups.clear();
|
||||||
|
|
||||||
// Group effects by category
|
// Group effects by category
|
||||||
|
|
@ -148,8 +148,8 @@ export class UIBuilder {
|
||||||
categorizeEffects(effects) {
|
categorizeEffects(effects) {
|
||||||
const categories = new Map();
|
const categories = new Map();
|
||||||
|
|
||||||
effects.forEach(effect => {
|
effects.forEach((effect) => {
|
||||||
const category = effect.category || 'Other';
|
const category = effect.category || "Other";
|
||||||
if (!categories.has(category)) {
|
if (!categories.has(category)) {
|
||||||
categories.set(category, []);
|
categories.set(category, []);
|
||||||
}
|
}
|
||||||
|
|
@ -159,17 +159,17 @@ export class UIBuilder {
|
||||||
// Sort categories in a logical order
|
// Sort categories in a logical order
|
||||||
const orderedCategories = new Map();
|
const orderedCategories = new Map();
|
||||||
const categoryOrder = [
|
const categoryOrder = [
|
||||||
'Text Line 1',
|
"Text Line 1",
|
||||||
'Text Line 2',
|
"Text Line 2",
|
||||||
'Background',
|
"Background",
|
||||||
'Background Animations',
|
"Background Animations",
|
||||||
'Border',
|
"Border",
|
||||||
'Visual Effects',
|
"Visual Effects",
|
||||||
'General Effects',
|
"General Effects",
|
||||||
'Special Effects'
|
"Special Effects",
|
||||||
];
|
];
|
||||||
|
|
||||||
categoryOrder.forEach(cat => {
|
categoryOrder.forEach((cat) => {
|
||||||
if (categories.has(cat)) {
|
if (categories.has(cat)) {
|
||||||
orderedCategories.set(cat, categories.get(cat));
|
orderedCategories.set(cat, categories.get(cat));
|
||||||
}
|
}
|
||||||
|
|
@ -191,29 +191,29 @@ export class UIBuilder {
|
||||||
* @param {Array<ButtonEffect>} effects - Effects in this category
|
* @param {Array<ButtonEffect>} effects - Effects in this category
|
||||||
*/
|
*/
|
||||||
createControlGroup(category, effects) {
|
createControlGroup(category, effects) {
|
||||||
const groupDiv = document.createElement('div');
|
const groupDiv = document.createElement("div");
|
||||||
groupDiv.className = 'control-group';
|
groupDiv.className = "control-group";
|
||||||
|
|
||||||
// Create header
|
// Create header
|
||||||
const header = document.createElement('h3');
|
const header = document.createElement("h3");
|
||||||
header.className = 'control-group-header';
|
header.className = "control-group-header";
|
||||||
header.innerHTML = `
|
header.innerHTML = `
|
||||||
<span>${category}</span>
|
<span>${category}</span>
|
||||||
<span class="toggle-icon">−</span>
|
<span class="toggle-icon">−</span>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// Create content container
|
// Create content container
|
||||||
const content = document.createElement('div');
|
const content = document.createElement("div");
|
||||||
content.className = 'control-group-content';
|
content.className = "control-group-content";
|
||||||
|
|
||||||
// Add controls for each effect in this category
|
// Add controls for each effect in this category
|
||||||
effects.forEach(effect => {
|
effects.forEach((effect) => {
|
||||||
this.addEffectControls(content, effect);
|
this.addEffectControls(content, effect);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add click handler for collapsing
|
// Add click handler for collapsing
|
||||||
header.addEventListener('click', () => {
|
header.addEventListener("click", () => {
|
||||||
groupDiv.classList.toggle('collapsed');
|
groupDiv.classList.toggle("collapsed");
|
||||||
});
|
});
|
||||||
|
|
||||||
groupDiv.appendChild(header);
|
groupDiv.appendChild(header);
|
||||||
|
|
@ -229,7 +229,7 @@ export class UIBuilder {
|
||||||
* @param {ButtonEffect} effect - Effect to create controls for
|
* @param {ButtonEffect} effect - Effect to create controls for
|
||||||
*/
|
*/
|
||||||
addEffectControls(container, effect) {
|
addEffectControls(container, effect) {
|
||||||
effect.controls.forEach(control => {
|
effect.controls.forEach((control) => {
|
||||||
const controlEl = this.createControl(control);
|
const controlEl = this.createControl(control);
|
||||||
if (controlEl) {
|
if (controlEl) {
|
||||||
container.appendChild(controlEl);
|
container.appendChild(controlEl);
|
||||||
|
|
@ -243,26 +243,85 @@ export class UIBuilder {
|
||||||
* @returns {HTMLElement}
|
* @returns {HTMLElement}
|
||||||
*/
|
*/
|
||||||
createControl(controlDef) {
|
createControl(controlDef) {
|
||||||
const { id, type, label, defaultValue, min, max, step, options, showWhen, description } = controlDef;
|
const {
|
||||||
|
id,
|
||||||
|
type,
|
||||||
|
label,
|
||||||
|
defaultValue,
|
||||||
|
min,
|
||||||
|
max,
|
||||||
|
step,
|
||||||
|
options,
|
||||||
|
showWhen,
|
||||||
|
description,
|
||||||
|
} = controlDef;
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'checkbox':
|
case "checkbox":
|
||||||
return this.createCheckbox(id, label, defaultValue, showWhen, description);
|
return this.createCheckbox(
|
||||||
|
id,
|
||||||
|
label,
|
||||||
|
defaultValue,
|
||||||
|
showWhen,
|
||||||
|
description,
|
||||||
|
);
|
||||||
|
|
||||||
case 'range':
|
case "range":
|
||||||
return this.createRange(id, label, defaultValue, min, max, step, description, showWhen);
|
return this.createRange(
|
||||||
|
id,
|
||||||
|
label,
|
||||||
|
defaultValue,
|
||||||
|
min,
|
||||||
|
max,
|
||||||
|
step,
|
||||||
|
description,
|
||||||
|
showWhen,
|
||||||
|
);
|
||||||
|
|
||||||
case 'color':
|
case "range-dual":
|
||||||
|
return this.createRangeDual(
|
||||||
|
id,
|
||||||
|
label,
|
||||||
|
controlDef.defaultValueStart,
|
||||||
|
controlDef.defaultValueEnd,
|
||||||
|
min,
|
||||||
|
max,
|
||||||
|
step,
|
||||||
|
description,
|
||||||
|
showWhen,
|
||||||
|
);
|
||||||
|
|
||||||
|
case "color":
|
||||||
return this.createColor(id, label, defaultValue, showWhen, description);
|
return this.createColor(id, label, defaultValue, showWhen, description);
|
||||||
|
|
||||||
case 'select':
|
case "select":
|
||||||
return this.createSelect(id, label, defaultValue, options, showWhen, description);
|
return this.createSelect(
|
||||||
|
id,
|
||||||
|
label,
|
||||||
|
defaultValue,
|
||||||
|
options,
|
||||||
|
showWhen,
|
||||||
|
description,
|
||||||
|
);
|
||||||
|
|
||||||
case 'text':
|
case "text":
|
||||||
return this.createTextInput(id, label, defaultValue, showWhen, description);
|
return this.createTextInput(
|
||||||
|
id,
|
||||||
|
label,
|
||||||
|
defaultValue,
|
||||||
|
showWhen,
|
||||||
|
description,
|
||||||
|
);
|
||||||
|
|
||||||
case 'file':
|
case "file":
|
||||||
return this.createFileInput(id, label, defaultValue, showWhen, description, controlDef.accept);
|
return this.createFileInput(
|
||||||
|
id,
|
||||||
|
label,
|
||||||
|
defaultValue,
|
||||||
|
showWhen,
|
||||||
|
description,
|
||||||
|
controlDef.accept,
|
||||||
|
);
|
||||||
|
|
||||||
default:
|
default:
|
||||||
console.warn(`Unknown control type: ${type}`);
|
console.warn(`Unknown control type: ${type}`);
|
||||||
|
|
@ -274,22 +333,22 @@ export class UIBuilder {
|
||||||
* Create a checkbox control
|
* Create a checkbox control
|
||||||
*/
|
*/
|
||||||
createCheckbox(id, label, defaultValue, showWhen, description) {
|
createCheckbox(id, label, defaultValue, showWhen, description) {
|
||||||
const wrapper = document.createElement('label');
|
const wrapper = document.createElement("label");
|
||||||
wrapper.className = 'checkbox-label';
|
wrapper.className = "checkbox-label";
|
||||||
|
|
||||||
const input = document.createElement('input');
|
const input = document.createElement("input");
|
||||||
input.type = 'checkbox';
|
input.type = "checkbox";
|
||||||
input.id = id;
|
input.id = id;
|
||||||
input.checked = defaultValue || false;
|
input.checked = defaultValue || false;
|
||||||
|
|
||||||
const span = document.createElement('span');
|
const span = document.createElement("span");
|
||||||
span.textContent = label;
|
span.textContent = label;
|
||||||
|
|
||||||
wrapper.appendChild(input);
|
wrapper.appendChild(input);
|
||||||
wrapper.appendChild(span);
|
wrapper.appendChild(span);
|
||||||
|
|
||||||
if (showWhen) {
|
if (showWhen) {
|
||||||
wrapper.style.display = 'none';
|
wrapper.style.display = "none";
|
||||||
wrapper.dataset.showWhen = showWhen;
|
wrapper.dataset.showWhen = showWhen;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -303,14 +362,14 @@ export class UIBuilder {
|
||||||
* Create a range slider control
|
* Create a range slider control
|
||||||
*/
|
*/
|
||||||
createRange(id, label, defaultValue, min, max, step, description, showWhen) {
|
createRange(id, label, defaultValue, min, max, step, description, showWhen) {
|
||||||
const container = document.createElement('div');
|
const container = document.createElement("div");
|
||||||
|
|
||||||
const labelEl = document.createElement('label');
|
const labelEl = document.createElement("label");
|
||||||
labelEl.htmlFor = id;
|
labelEl.htmlFor = id;
|
||||||
labelEl.innerHTML = `${label}: <span id="${id}-value">${defaultValue}</span>`;
|
labelEl.innerHTML = `${label}: <span id="${id}-value">${defaultValue}</span>`;
|
||||||
|
|
||||||
const input = document.createElement('input');
|
const input = document.createElement("input");
|
||||||
input.type = 'range';
|
input.type = "range";
|
||||||
input.id = id;
|
input.id = id;
|
||||||
input.min = min !== undefined ? min : 0;
|
input.min = min !== undefined ? min : 0;
|
||||||
input.max = max !== undefined ? max : 100;
|
input.max = max !== undefined ? max : 100;
|
||||||
|
|
@ -320,7 +379,7 @@ export class UIBuilder {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update value display on input
|
// Update value display on input
|
||||||
input.addEventListener('input', () => {
|
input.addEventListener("input", () => {
|
||||||
const valueDisplay = document.getElementById(`${id}-value`);
|
const valueDisplay = document.getElementById(`${id}-value`);
|
||||||
if (valueDisplay) {
|
if (valueDisplay) {
|
||||||
valueDisplay.textContent = input.value;
|
valueDisplay.textContent = input.value;
|
||||||
|
|
@ -331,7 +390,7 @@ export class UIBuilder {
|
||||||
container.appendChild(input);
|
container.appendChild(input);
|
||||||
|
|
||||||
if (showWhen) {
|
if (showWhen) {
|
||||||
container.style.display = 'none';
|
container.style.display = "none";
|
||||||
container.dataset.showWhen = showWhen;
|
container.dataset.showWhen = showWhen;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -341,26 +400,239 @@ export class UIBuilder {
|
||||||
return container;
|
return container;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a dual-range slider control (two handles on one track)
|
||||||
|
*/
|
||||||
|
createRangeDual(
|
||||||
|
id,
|
||||||
|
label,
|
||||||
|
defaultValueStart,
|
||||||
|
defaultValueEnd,
|
||||||
|
min,
|
||||||
|
max,
|
||||||
|
step,
|
||||||
|
description,
|
||||||
|
showWhen,
|
||||||
|
) {
|
||||||
|
const container = document.createElement("div");
|
||||||
|
container.className = "range-dual-container";
|
||||||
|
|
||||||
|
const labelEl = document.createElement("label");
|
||||||
|
labelEl.innerHTML = `${label}: <span id="${id}-value">${defaultValueStart}-${defaultValueEnd}</span>`;
|
||||||
|
|
||||||
|
// Create wrapper for the dual range slider
|
||||||
|
const sliderWrapper = document.createElement("div");
|
||||||
|
sliderWrapper.className = "range-dual-wrapper";
|
||||||
|
sliderWrapper.style.cssText = `
|
||||||
|
position: relative;
|
||||||
|
height: 30px;
|
||||||
|
margin: 10px 0;
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Create the track
|
||||||
|
const track = document.createElement("div");
|
||||||
|
track.className = "range-dual-track";
|
||||||
|
track.style.cssText = `
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
width: 100%;
|
||||||
|
height: 4px;
|
||||||
|
background: #444;
|
||||||
|
border-radius: 2px;
|
||||||
|
z-index: 1;
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Create the filled range indicator
|
||||||
|
const range = document.createElement("div");
|
||||||
|
range.className = "range-dual-range";
|
||||||
|
range.id = `${id}-range`;
|
||||||
|
range.style.cssText = `
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
height: 4px;
|
||||||
|
background: linear-gradient(90deg, #ff9966, #00bcf2); // Change this to orange
|
||||||
|
border-radius: 2px;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 10;
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Create start handle input
|
||||||
|
const inputStart = document.createElement("input");
|
||||||
|
inputStart.type = "range";
|
||||||
|
inputStart.id = `${id}-start`;
|
||||||
|
inputStart.className = "range-dual-input";
|
||||||
|
inputStart.min = min !== undefined ? min : 0;
|
||||||
|
inputStart.max = max !== undefined ? max : 100;
|
||||||
|
inputStart.value =
|
||||||
|
defaultValueStart !== undefined ? defaultValueStart : min || 0;
|
||||||
|
if (step !== undefined) {
|
||||||
|
inputStart.step = step;
|
||||||
|
}
|
||||||
|
inputStart.style.cssText = `
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
background: transparent;
|
||||||
|
pointer-events: none;
|
||||||
|
margin: 0;
|
||||||
|
z-index: 3;
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Create end handle input
|
||||||
|
const inputEnd = document.createElement("input");
|
||||||
|
inputEnd.type = "range";
|
||||||
|
inputEnd.id = `${id}-end`;
|
||||||
|
inputEnd.className = "range-dual-input";
|
||||||
|
inputEnd.min = min !== undefined ? min : 0;
|
||||||
|
inputEnd.max = max !== undefined ? max : 100;
|
||||||
|
inputEnd.value =
|
||||||
|
defaultValueEnd !== undefined ? defaultValueEnd : max || 100;
|
||||||
|
if (step !== undefined) {
|
||||||
|
inputEnd.step = step;
|
||||||
|
}
|
||||||
|
inputEnd.style.cssText = `
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
background: transparent;
|
||||||
|
pointer-events: none;
|
||||||
|
margin: 0;
|
||||||
|
z-index: 4;
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Add CSS for the range inputs
|
||||||
|
const style = document.createElement("style");
|
||||||
|
style.textContent = `
|
||||||
|
.range-dual-input::-webkit-slider-runnable-track {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
height: 4px;
|
||||||
|
}
|
||||||
|
.range-dual-input::-moz-range-track {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
height: 4px;
|
||||||
|
}
|
||||||
|
.range-dual-input::-webkit-slider-thumb {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #0078d4;
|
||||||
|
cursor: pointer;
|
||||||
|
pointer-events: all;
|
||||||
|
border: 2px solid #fff;
|
||||||
|
box-shadow: 0 0 5px rgba(0, 0, 0, 0.5);
|
||||||
|
position: relative;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
.range-dual-input::-moz-range-thumb {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #0078d4;
|
||||||
|
cursor: pointer;
|
||||||
|
pointer-events: all;
|
||||||
|
border: 2px solid #fff;
|
||||||
|
box-shadow: 0 0 5px rgba(0, 0, 0, 0.5);
|
||||||
|
position: relative;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
.range-dual-input::-webkit-slider-thumb:hover {
|
||||||
|
background: #00bcf2;
|
||||||
|
}
|
||||||
|
.range-dual-input::-moz-range-thumb:hover {
|
||||||
|
background: #00bcf2;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
if (!document.getElementById("range-dual-styles")) {
|
||||||
|
style.id = "range-dual-styles";
|
||||||
|
document.head.appendChild(style);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update function
|
||||||
|
const updateRange = () => {
|
||||||
|
let startVal = parseInt(inputStart.value);
|
||||||
|
let endVal = parseInt(inputEnd.value);
|
||||||
|
|
||||||
|
// Ensure start is never greater than end
|
||||||
|
if (startVal > endVal) {
|
||||||
|
const temp = startVal;
|
||||||
|
startVal = endVal;
|
||||||
|
endVal = temp;
|
||||||
|
inputStart.value = startVal;
|
||||||
|
inputEnd.value = endVal;
|
||||||
|
}
|
||||||
|
|
||||||
|
const minVal = parseInt(inputStart.min);
|
||||||
|
const maxVal = parseInt(inputStart.max);
|
||||||
|
const startPercent = ((startVal - minVal) / (maxVal - minVal)) * 100;
|
||||||
|
const endPercent = ((endVal - minVal) / (maxVal - minVal)) * 100;
|
||||||
|
|
||||||
|
range.style.left = `${startPercent}%`;
|
||||||
|
range.style.width = `${endPercent - startPercent}%`;
|
||||||
|
|
||||||
|
const valueDisplay = document.getElementById(`${id}-value`);
|
||||||
|
if (valueDisplay) {
|
||||||
|
valueDisplay.textContent = `${startVal}-${endVal}`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
inputStart.addEventListener("input", updateRange);
|
||||||
|
inputEnd.addEventListener("input", updateRange);
|
||||||
|
|
||||||
|
// Assemble the dual range slider
|
||||||
|
sliderWrapper.appendChild(track);
|
||||||
|
sliderWrapper.appendChild(range);
|
||||||
|
sliderWrapper.appendChild(inputStart);
|
||||||
|
sliderWrapper.appendChild(inputEnd);
|
||||||
|
|
||||||
|
container.appendChild(labelEl);
|
||||||
|
container.appendChild(sliderWrapper);
|
||||||
|
|
||||||
|
if (showWhen) {
|
||||||
|
container.style.display = "none";
|
||||||
|
container.dataset.showWhen = showWhen;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add tooltip handlers to the label
|
||||||
|
this.addTooltipHandlers(labelEl, description);
|
||||||
|
|
||||||
|
// Initialize range display
|
||||||
|
updateRange();
|
||||||
|
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a color picker control
|
* Create a color picker control
|
||||||
*/
|
*/
|
||||||
createColor(id, label, defaultValue, showWhen, description) {
|
createColor(id, label, defaultValue, showWhen, description) {
|
||||||
const container = document.createElement('div');
|
const container = document.createElement("div");
|
||||||
|
|
||||||
const labelEl = document.createElement('label');
|
const labelEl = document.createElement("label");
|
||||||
labelEl.htmlFor = id;
|
labelEl.htmlFor = id;
|
||||||
labelEl.textContent = label;
|
labelEl.textContent = label;
|
||||||
|
|
||||||
const input = document.createElement('input');
|
const input = document.createElement("input");
|
||||||
input.type = 'color';
|
input.type = "color";
|
||||||
input.id = id;
|
input.id = id;
|
||||||
input.value = defaultValue || '#ffffff';
|
input.value = defaultValue || "#ffffff";
|
||||||
|
|
||||||
container.appendChild(labelEl);
|
container.appendChild(labelEl);
|
||||||
container.appendChild(input);
|
container.appendChild(input);
|
||||||
|
|
||||||
if (showWhen) {
|
if (showWhen) {
|
||||||
container.style.display = 'none';
|
container.style.display = "none";
|
||||||
container.dataset.showWhen = showWhen;
|
container.dataset.showWhen = showWhen;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -374,17 +646,17 @@ export class UIBuilder {
|
||||||
* Create a select dropdown control
|
* Create a select dropdown control
|
||||||
*/
|
*/
|
||||||
createSelect(id, label, defaultValue, options, showWhen, description) {
|
createSelect(id, label, defaultValue, options, showWhen, description) {
|
||||||
const container = document.createElement('div');
|
const container = document.createElement("div");
|
||||||
|
|
||||||
const labelEl = document.createElement('label');
|
const labelEl = document.createElement("label");
|
||||||
labelEl.htmlFor = id;
|
labelEl.htmlFor = id;
|
||||||
labelEl.textContent = label;
|
labelEl.textContent = label;
|
||||||
|
|
||||||
const select = document.createElement('select');
|
const select = document.createElement("select");
|
||||||
select.id = id;
|
select.id = id;
|
||||||
|
|
||||||
options.forEach(opt => {
|
options.forEach((opt) => {
|
||||||
const option = document.createElement('option');
|
const option = document.createElement("option");
|
||||||
option.value = opt.value;
|
option.value = opt.value;
|
||||||
option.textContent = opt.label;
|
option.textContent = opt.label;
|
||||||
if (opt.value === defaultValue) {
|
if (opt.value === defaultValue) {
|
||||||
|
|
@ -397,7 +669,7 @@ export class UIBuilder {
|
||||||
container.appendChild(select);
|
container.appendChild(select);
|
||||||
|
|
||||||
if (showWhen) {
|
if (showWhen) {
|
||||||
container.style.display = 'none';
|
container.style.display = "none";
|
||||||
container.dataset.showWhen = showWhen;
|
container.dataset.showWhen = showWhen;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -411,19 +683,19 @@ export class UIBuilder {
|
||||||
* Create a text input control
|
* Create a text input control
|
||||||
*/
|
*/
|
||||||
createTextInput(id, label, defaultValue, showWhen, description) {
|
createTextInput(id, label, defaultValue, showWhen, description) {
|
||||||
const container = document.createElement('div');
|
const container = document.createElement("div");
|
||||||
|
|
||||||
const labelEl = document.createElement('label');
|
const labelEl = document.createElement("label");
|
||||||
labelEl.htmlFor = id;
|
labelEl.htmlFor = id;
|
||||||
labelEl.textContent = label;
|
labelEl.textContent = label;
|
||||||
|
|
||||||
const input = document.createElement('input');
|
const input = document.createElement("input");
|
||||||
input.type = 'text';
|
input.type = "text";
|
||||||
input.id = id;
|
input.id = id;
|
||||||
input.value = defaultValue || '';
|
input.value = defaultValue || "";
|
||||||
|
|
||||||
// Only set maxLength for text inputs that aren't URLs
|
// Only set maxLength for text inputs that aren't URLs
|
||||||
if (id !== 'bg-image-url') {
|
if (id !== "bg-image-url") {
|
||||||
input.maxLength = 20;
|
input.maxLength = 20;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -431,7 +703,7 @@ export class UIBuilder {
|
||||||
container.appendChild(input);
|
container.appendChild(input);
|
||||||
|
|
||||||
if (showWhen) {
|
if (showWhen) {
|
||||||
container.style.display = 'none';
|
container.style.display = "none";
|
||||||
container.dataset.showWhen = showWhen;
|
container.dataset.showWhen = showWhen;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -445,21 +717,21 @@ export class UIBuilder {
|
||||||
* Create a file input control
|
* Create a file input control
|
||||||
*/
|
*/
|
||||||
createFileInput(id, label, defaultValue, showWhen, description, accept) {
|
createFileInput(id, label, defaultValue, showWhen, description, accept) {
|
||||||
const container = document.createElement('div');
|
const container = document.createElement("div");
|
||||||
|
|
||||||
const labelEl = document.createElement('label');
|
const labelEl = document.createElement("label");
|
||||||
labelEl.htmlFor = id;
|
labelEl.htmlFor = id;
|
||||||
labelEl.textContent = label;
|
labelEl.textContent = label;
|
||||||
|
|
||||||
const input = document.createElement('input');
|
const input = document.createElement("input");
|
||||||
input.type = 'file';
|
input.type = "file";
|
||||||
input.id = id;
|
input.id = id;
|
||||||
if (accept) {
|
if (accept) {
|
||||||
input.accept = accept;
|
input.accept = accept;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store the file data on the input element
|
// Store the file data on the input element
|
||||||
input.addEventListener('change', (e) => {
|
input.addEventListener("change", (e) => {
|
||||||
const file = e.target.files[0];
|
const file = e.target.files[0];
|
||||||
if (file) {
|
if (file) {
|
||||||
// Create a blob URL for the file
|
// Create a blob URL for the file
|
||||||
|
|
@ -482,7 +754,7 @@ export class UIBuilder {
|
||||||
container.appendChild(input);
|
container.appendChild(input);
|
||||||
|
|
||||||
if (showWhen) {
|
if (showWhen) {
|
||||||
container.style.display = 'none';
|
container.style.display = "none";
|
||||||
container.dataset.showWhen = showWhen;
|
container.dataset.showWhen = showWhen;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -498,64 +770,103 @@ export class UIBuilder {
|
||||||
*/
|
*/
|
||||||
setupConditionalVisibility() {
|
setupConditionalVisibility() {
|
||||||
// Find all controls with showWhen attribute
|
// Find all controls with showWhen attribute
|
||||||
const conditionalControls = this.container.querySelectorAll('[data-show-when]');
|
const conditionalControls =
|
||||||
|
this.container.querySelectorAll("[data-show-when]");
|
||||||
|
|
||||||
conditionalControls.forEach(control => {
|
conditionalControls.forEach((control) => {
|
||||||
const triggerControlId = control.dataset.showWhen;
|
const triggerControlId = control.dataset.showWhen;
|
||||||
const triggerControl = document.getElementById(triggerControlId);
|
const triggerControl = document.getElementById(triggerControlId);
|
||||||
|
|
||||||
if (triggerControl) {
|
if (triggerControl) {
|
||||||
const updateVisibility = () => {
|
const updateVisibility = () => {
|
||||||
if (triggerControl.type === 'checkbox') {
|
if (triggerControl.type === "checkbox") {
|
||||||
control.style.display = triggerControl.checked ? 'block' : 'none';
|
control.style.display = triggerControl.checked ? "block" : "none";
|
||||||
} else if (triggerControl.tagName === 'SELECT') {
|
} else if (triggerControl.tagName === "SELECT") {
|
||||||
// Get the control ID to determine what value to check for
|
// Get the control ID to determine what value to check for
|
||||||
const controlId = control.querySelector('input, select')?.id;
|
const controlId = control.querySelector("input, select")?.id;
|
||||||
|
|
||||||
// For background controls
|
// For background controls
|
||||||
if (triggerControlId === 'bg-type') {
|
if (triggerControlId === "bg-type") {
|
||||||
if (controlId === 'bg-color') {
|
if (controlId === "bg-color") {
|
||||||
control.style.display = triggerControl.value === 'solid' ? 'block' : 'none';
|
control.style.display =
|
||||||
} else if (controlId && (controlId.startsWith('gradient-') || controlId === 'gradient-angle')) {
|
triggerControl.value === "solid" ||
|
||||||
control.style.display = triggerControl.value === 'gradient' ? 'block' : 'none';
|
triggerControl.value === "external-image"
|
||||||
} else if (controlId && (controlId.startsWith('texture-') || controlId === 'texture-type' || controlId === 'texture-scale')) {
|
? "block"
|
||||||
control.style.display = triggerControl.value === 'texture' ? 'block' : 'none';
|
: "none";
|
||||||
} else if (controlId && (controlId.startsWith('emoji-') || controlId === 'emoji-text')) {
|
} else if (
|
||||||
control.style.display = triggerControl.value === 'emoji-wallpaper' ? 'block' : 'none';
|
controlId &&
|
||||||
} else if (controlId && (controlId.startsWith('bg-image-') || controlId === 'bg-image-file' || controlId === 'bg-image-fit' || controlId === 'bg-image-opacity')) {
|
(controlId.startsWith("gradient-") ||
|
||||||
control.style.display = triggerControl.value === 'external-image' ? 'block' : 'none';
|
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";
|
||||||
|
} else if (
|
||||||
|
controlId &&
|
||||||
|
(controlId.startsWith("bg-image-") ||
|
||||||
|
controlId === "bg-image-file" ||
|
||||||
|
controlId === "bg-image-fit" ||
|
||||||
|
controlId === "bg-image-opacity")
|
||||||
|
) {
|
||||||
|
control.style.display =
|
||||||
|
triggerControl.value === "external-image" ? "block" : "none";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// For image fit controls (zoom and position only show when manual mode)
|
// For image fit controls (zoom and position only show when manual mode)
|
||||||
else if (triggerControlId === 'bg-image-fit') {
|
else if (triggerControlId === "bg-image-fit") {
|
||||||
if (controlId && (controlId === 'bg-image-zoom' || controlId === 'bg-image-offset-x' || controlId === 'bg-image-offset-y')) {
|
if (
|
||||||
control.style.display = triggerControl.value === 'manual' ? 'block' : 'none';
|
controlId &&
|
||||||
|
(controlId === "bg-image-zoom" ||
|
||||||
|
controlId === "bg-image-offset-x" ||
|
||||||
|
controlId === "bg-image-offset-y")
|
||||||
|
) {
|
||||||
|
control.style.display =
|
||||||
|
triggerControl.value === "manual" ? "block" : "none";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// For text color controls
|
// For text color controls
|
||||||
else if (triggerControlId === 'text-color-type') {
|
else if (triggerControlId === "text-color-type") {
|
||||||
if (controlId === 'text-color') {
|
if (controlId === "text-color") {
|
||||||
control.style.display = triggerControl.value === 'solid' ? 'block' : 'none';
|
control.style.display =
|
||||||
} else if (controlId && controlId.startsWith('text-gradient-')) {
|
triggerControl.value === "solid" ? "block" : "none";
|
||||||
control.style.display = triggerControl.value === 'gradient' ? 'block' : 'none';
|
} else if (controlId && controlId.startsWith("text-gradient-")) {
|
||||||
|
control.style.display =
|
||||||
|
triggerControl.value === "gradient" ? "block" : "none";
|
||||||
}
|
}
|
||||||
} else if (triggerControlId === 'text2-color-type') {
|
} else if (triggerControlId === "text2-color-type") {
|
||||||
if (controlId === 'text2-color') {
|
if (controlId === "text2-color") {
|
||||||
control.style.display = triggerControl.value === 'solid' ? 'block' : 'none';
|
control.style.display =
|
||||||
} else if (controlId && controlId.startsWith('text2-gradient-')) {
|
triggerControl.value === "solid" ? "block" : "none";
|
||||||
control.style.display = triggerControl.value === 'gradient' ? 'block' : 'none';
|
} else if (controlId && controlId.startsWith("text2-gradient-")) {
|
||||||
|
control.style.display =
|
||||||
|
triggerControl.value === "gradient" ? "block" : "none";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// For border style controls
|
// For border style controls
|
||||||
else if (triggerControlId === 'border-style') {
|
else if (triggerControlId === "border-style") {
|
||||||
if (controlId === 'border-rainbow-speed') {
|
if (controlId === "border-rainbow-speed") {
|
||||||
control.style.display = triggerControl.value === 'rainbow' ? 'block' : 'none';
|
control.style.display =
|
||||||
} else if (controlId === 'border-march-speed') {
|
triggerControl.value === "rainbow" ? "block" : "none";
|
||||||
control.style.display = triggerControl.value === 'marching-ants' ? 'block' : 'none';
|
} else if (controlId === "border-march-speed") {
|
||||||
|
control.style.display =
|
||||||
|
triggerControl.value === "marching-ants" ? "block" : "none";
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Default: show when any value is selected
|
// Default: show when any value is selected
|
||||||
control.style.display = triggerControl.value ? 'block' : 'none';
|
control.style.display = triggerControl.value ? "block" : "none";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -564,8 +875,8 @@ export class UIBuilder {
|
||||||
updateVisibility();
|
updateVisibility();
|
||||||
|
|
||||||
// Update on change
|
// Update on change
|
||||||
triggerControl.addEventListener('change', updateVisibility);
|
triggerControl.addEventListener("change", updateVisibility);
|
||||||
triggerControl.addEventListener('input', updateVisibility);
|
triggerControl.addEventListener("input", updateVisibility);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue