Loads of button generator additions
This commit is contained in:
parent
98b4edf47d
commit
e7754141bf
12 changed files with 904 additions and 126 deletions
|
|
@ -245,12 +245,17 @@ export class ExternalImageBackgroundEffect extends ButtonEffect {
|
|||
|
||||
apply(context, controlValues, animState, renderData) {
|
||||
const file = controlValues['bg-image-file'];
|
||||
const bgColor = controlValues['bg-color'] || '#FFFFFF';
|
||||
const fitMode = controlValues['bg-image-fit'] || 'cover';
|
||||
const opacity = controlValues['bg-image-opacity'] ?? 1;
|
||||
const zoom = controlValues['bg-image-zoom'] ?? 100;
|
||||
const offsetX = controlValues['bg-image-offset-x'] ?? 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 (!file || !file.blobUrl) {
|
||||
context.fillStyle = '#cccccc';
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ export class SolidBackgroundEffect extends ButtonEffect {
|
|||
label: "Background Color",
|
||||
defaultValue: "#4a90e2",
|
||||
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';
|
||||
|
||||
// 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
|
||||
const text = controlValues[`button-text${suffix}`] || '';
|
||||
if (!text || text.trim() === '') return;
|
||||
|
|
|
|||
|
|
@ -61,6 +61,19 @@ export class SpinTextEffect extends ButtonEffect {
|
|||
|
||||
apply(context, controlValues, animState, renderData) {
|
||||
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}`] || "";
|
||||
|
||||
if (!text || text.trim() === '') return;
|
||||
|
|
|
|||
|
|
@ -98,6 +98,12 @@ export class TextShadowEffect extends ButtonEffect {
|
|||
apply(context, controlValues, animState, renderData) {
|
||||
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}`] || "";
|
||||
if (!text) return;
|
||||
|
||||
|
|
|
|||
|
|
@ -150,13 +150,20 @@ export class StandardTextEffect extends ButtonEffect {
|
|||
const waveActive = controlValues[`animate-text-wave${suffix}`];
|
||||
const rainbowActive = controlValues[`animate-text-rainbow${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) {
|
||||
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}`];
|
||||
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';
|
||||
|
||||
// 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
|
||||
const text = controlValues[`button-text${suffix}`] || '';
|
||||
if (!text || text.trim() === '') return;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue