Adding some new effects to button generator

This commit is contained in:
Dan 2026-01-22 07:08:32 +00:00
parent d0c65a71ad
commit f980d65c86
7 changed files with 1023 additions and 1 deletions

View file

@ -0,0 +1,208 @@
import { ButtonEffect } from "../effect-base.js";
/**
* Plasma background effect
* Classic demoscene sine wave plasma - iconic 90s visual
*/
export class PlasmaEffect extends ButtonEffect {
constructor() {
super({
id: "bg-plasma",
name: "Plasma",
type: "background-animation",
category: "Background Animations",
renderOrder: 5,
});
}
defineControls() {
return [
{
id: "animate-plasma",
type: "checkbox",
label: "Plasma Effect",
defaultValue: false,
},
{
id: "plasma-scale",
type: "range",
label: "Pattern Scale",
defaultValue: 1.5,
min: 0.5,
max: 3,
step: 0.1,
showWhen: "animate-plasma",
description: "Size of plasma patterns",
},
{
id: "plasma-speed",
type: "range",
label: "Animation Speed",
defaultValue: 1,
min: 0.5,
max: 3,
step: 0.1,
showWhen: "animate-plasma",
description: "Speed of plasma movement",
},
{
id: "plasma-color-scheme",
type: "select",
label: "Color Scheme",
defaultValue: "classic",
options: [
{ value: "classic", label: "Classic (Purple/Cyan)" },
{ value: "fire", label: "Fire (Red/Orange)" },
{ value: "ocean", label: "Ocean (Blue/Green)" },
{ value: "psychedelic", label: "Psychedelic (Rainbow)" },
{ value: "matrix", label: "Matrix (Green)" },
],
showWhen: "animate-plasma",
description: "Color palette for plasma",
},
];
}
isEnabled(controlValues) {
return controlValues["animate-plasma"] === true;
}
/**
* Get color based on scheme and value
*/
getColor(value, scheme) {
// Normalize value from -4..4 to 0..1
const normalized = (value + 4) / 8;
switch (scheme) {
case "fire": {
// Black -> Red -> Orange -> Yellow -> White
const r = Math.min(255, normalized * 400);
const g = Math.max(0, Math.min(255, (normalized - 0.3) * 400));
const b = Math.max(0, Math.min(255, (normalized - 0.7) * 400));
return { r, g, b };
}
case "ocean": {
// Deep blue -> Cyan -> Light green
const r = Math.max(0, Math.min(100, normalized * 100));
const g = Math.min(255, normalized * 300);
const b = Math.min(255, 100 + normalized * 155);
return { r, g, b };
}
case "psychedelic": {
// Full rainbow cycling
const hue = normalized * 360;
return this.hslToRgb(hue, 100, 50);
}
case "matrix": {
// Black -> Dark green -> Bright green -> White
const intensity = normalized;
const r = Math.max(0, Math.min(255, (intensity - 0.8) * 1000));
const g = Math.min(255, intensity * 300);
const b = Math.max(0, Math.min(255, (intensity - 0.9) * 500));
return { r, g, b };
}
case "classic":
default: {
// Classic demoscene: purple/magenta/cyan
const r = Math.min(255, 128 + Math.sin(normalized * Math.PI * 2) * 127);
const g = Math.min(255, 64 + Math.sin(normalized * Math.PI * 2 + 2) * 64);
const b = Math.min(255, 128 + Math.sin(normalized * Math.PI * 2 + 4) * 127);
return { r, g, b };
}
}
}
/**
* Convert HSL to RGB
*/
hslToRgb(h, s, l) {
s /= 100;
l /= 100;
const c = (1 - Math.abs(2 * l - 1)) * s;
const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
const m = l - c / 2;
let r, g, b;
if (h < 60) {
r = c; g = x; b = 0;
} else if (h < 120) {
r = x; g = c; b = 0;
} else if (h < 180) {
r = 0; g = c; b = x;
} else if (h < 240) {
r = 0; g = x; b = c;
} else if (h < 300) {
r = x; g = 0; b = c;
} else {
r = c; g = 0; b = x;
}
return {
r: Math.round((r + m) * 255),
g: Math.round((g + m) * 255),
b: Math.round((b + m) * 255),
};
}
apply(context, controlValues, animState, renderData) {
if (!animState) return;
const scale = controlValues["plasma-scale"] || 1.5;
const speed = controlValues["plasma-speed"] || 1;
const scheme = controlValues["plasma-color-scheme"] || "classic";
const { width, height } = renderData;
// Perfect looping via ping-pong: animate forward first half, reverse second half
// progress goes 0->1, we convert to 0->1->0 (triangle wave)
const progress = animState.progress;
let pingPong;
if (progress < 0.5) {
// First half: 0->1
pingPong = progress * 2;
} else {
// Second half: 1->0
pingPong = (1 - progress) * 2;
}
// Scale the time value for visible animation
const time = pingPong * Math.PI * 2 * speed;
// Get image data for pixel manipulation
const imageData = context.getImageData(0, 0, width, height);
const data = imageData.data;
// Scale factor for the plasma pattern (smaller = larger patterns)
const scaleFactor = 10 / scale;
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
// Classic plasma formula
const value =
Math.sin(x / scaleFactor + time) +
Math.sin(y / scaleFactor + time * 1.3) +
Math.sin((x + y) / scaleFactor + time * 0.7) +
Math.sin(Math.sqrt(x * x + y * y) / scaleFactor + time);
const color = this.getColor(value, scheme);
const idx = (y * width + x) * 4;
data[idx] = color.r;
data[idx + 1] = color.g;
data[idx + 2] = color.b;
data[idx + 3] = 255;
}
}
context.putImageData(imageData, 0, 0);
}
}
export function register(generator) {
generator.registerEffect(new PlasmaEffect());
}

View file

@ -0,0 +1,168 @@
import { ButtonEffect } from "../effect-base.js";
/**
* Sparkle/Twinkle background effect
* Random twinkling stars overlay - classic 88x31 button aesthetic
*/
export class SparkleEffect extends ButtonEffect {
constructor() {
super({
id: "bg-sparkle",
name: "Sparkle",
type: "background-animation",
category: "Background Animations",
renderOrder: 15,
});
this.sparkles = [];
this.initialized = false;
}
defineControls() {
return [
{
id: "animate-sparkle",
type: "checkbox",
label: "Sparkle Effect",
defaultValue: false,
},
{
id: "sparkle-density",
type: "range",
label: "Sparkle Count",
defaultValue: 20,
min: 5,
max: 50,
step: 1,
showWhen: "animate-sparkle",
description: "Number of sparkles",
},
{
id: "sparkle-size",
type: "range",
label: "Max Size",
defaultValue: 3,
min: 1,
max: 5,
step: 0.5,
showWhen: "animate-sparkle",
description: "Maximum sparkle size",
},
{
id: "sparkle-speed",
type: "range",
label: "Twinkle Speed",
defaultValue: 1.5,
min: 0.5,
max: 3,
step: 0.1,
showWhen: "animate-sparkle",
description: "How fast sparkles twinkle",
},
{
id: "sparkle-color",
type: "color",
label: "Sparkle Color",
defaultValue: "#ffffff",
showWhen: "animate-sparkle",
description: "Color of sparkles",
},
];
}
isEnabled(controlValues) {
return controlValues["animate-sparkle"] === true;
}
apply(context, controlValues, animState, renderData) {
if (!animState) return;
const density = controlValues["sparkle-density"] || 20;
const maxSize = controlValues["sparkle-size"] || 3;
const speed = controlValues["sparkle-speed"] || 1.5;
const sparkleColor = controlValues["sparkle-color"] || "#ffffff";
// Initialize sparkles on first frame or density change
if (!this.initialized || this.sparkles.length !== density) {
this.sparkles = [];
for (let i = 0; i < density; i++) {
this.sparkles.push({
x: Math.random() * renderData.width,
y: Math.random() * renderData.height,
phase: Math.random() * Math.PI * 2,
size: 1 + Math.random() * (maxSize - 1),
speedMult: 0.7 + Math.random() * 0.6,
});
}
this.initialized = true;
}
// Parse color
const hexToRgb = (hex) => {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result
? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16),
}
: { r: 255, g: 255, b: 255 };
};
const rgb = hexToRgb(sparkleColor);
// Draw sparkles
this.sparkles.forEach((sparkle) => {
// Calculate twinkle phase - each sparkle has its own timing
const phase = animState.getPhase(speed * sparkle.speedMult) + sparkle.phase;
// Use sin² for smooth fade in/out, with some sparkles fully off
const rawAlpha = Math.sin(phase);
const alpha = rawAlpha > 0 ? rawAlpha * rawAlpha : 0;
if (alpha < 0.05) return; // Skip nearly invisible sparkles
const size = sparkle.size * (0.5 + alpha * 0.5);
// Draw 4-point star shape
context.save();
context.translate(sparkle.x, sparkle.y);
context.fillStyle = `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${alpha})`;
// Draw diamond/star shape
context.beginPath();
// Horizontal points
context.moveTo(-size, 0);
context.lineTo(0, -size * 0.3);
context.lineTo(size, 0);
context.lineTo(0, size * 0.3);
context.closePath();
context.fill();
// Vertical points
context.beginPath();
context.moveTo(0, -size);
context.lineTo(size * 0.3, 0);
context.lineTo(0, size);
context.lineTo(-size * 0.3, 0);
context.closePath();
context.fill();
// Center glow
const gradient = context.createRadialGradient(0, 0, 0, 0, 0, size * 0.5);
gradient.addColorStop(0, `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${alpha})`);
gradient.addColorStop(1, `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0)`);
context.fillStyle = gradient;
context.beginPath();
context.arc(0, 0, size * 0.5, 0, Math.PI * 2);
context.fill();
context.restore();
});
}
}
export function register(generator) {
generator.registerEffect(new SparkleEffect());
}

View file

@ -0,0 +1,245 @@
import { ButtonEffect } from "../effect-base.js";
/**
* Emboss/Bevel border effect
* Classic Windows 95/98 raised button appearance
*/
export class EmbossEffect extends ButtonEffect {
constructor() {
super({
id: "emboss",
name: "Emboss/Bevel",
type: "general",
category: "Visual Effects",
renderOrder: 95, // After everything, draws on top
});
}
defineControls() {
return [
{
id: "enable-emboss",
type: "checkbox",
label: "Emboss/Bevel Effect",
defaultValue: false,
},
{
id: "emboss-style",
type: "select",
label: "Style",
defaultValue: "raised",
options: [
{ value: "raised", label: "Raised (Outset)" },
{ value: "sunken", label: "Sunken (Inset)" },
{ value: "ridge", label: "Ridge" },
{ value: "groove", label: "Groove" },
],
showWhen: "enable-emboss",
description: "Type of bevel effect",
},
{
id: "emboss-depth",
type: "range",
label: "Depth",
defaultValue: 2,
min: 1,
max: 4,
step: 1,
showWhen: "enable-emboss",
description: "Thickness of bevel in pixels",
},
{
id: "emboss-light-color",
type: "color",
label: "Highlight Color",
defaultValue: "#ffffff",
showWhen: "enable-emboss",
description: "Color for lit edges",
},
{
id: "emboss-shadow-color",
type: "color",
label: "Shadow Color",
defaultValue: "#000000",
showWhen: "enable-emboss",
description: "Color for shadowed edges",
},
];
}
isEnabled(controlValues) {
return controlValues["enable-emboss"] === true;
}
/**
* Parse hex color to rgba string with alpha
*/
colorWithAlpha(hex, alpha) {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
if (result) {
const r = parseInt(result[1], 16);
const g = parseInt(result[2], 16);
const b = parseInt(result[3], 16);
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
}
return `rgba(255, 255, 255, ${alpha})`;
}
/**
* Draw a single bevel layer
*/
drawBevel(
context,
width,
height,
offset,
lightColor,
shadowColor,
lightAlpha,
shadowAlpha,
) {
// Top edge (light)
context.fillStyle = this.colorWithAlpha(lightColor, lightAlpha);
context.fillRect(offset, offset, width - offset * 2, 1);
// Left edge (light)
context.fillRect(offset, offset, 1, height - offset * 2);
// Bottom edge (shadow)
context.fillStyle = this.colorWithAlpha(shadowColor, shadowAlpha);
context.fillRect(offset, height - 1 - offset, width - offset * 2, 1);
// Right edge (shadow)
context.fillRect(width - 1 - offset, offset, 1, height - offset * 2);
}
apply(context, controlValues, animState, renderData) {
const style = controlValues["emboss-style"] || "raised";
const depth = controlValues["emboss-depth"] || 2;
const lightColor = controlValues["emboss-light-color"] || "#ffffff";
const shadowColor = controlValues["emboss-shadow-color"] || "#000000";
const { width, height } = renderData;
context.save();
// Calculate alpha falloff for each layer (outer layers more opaque)
const getAlpha = (layer, totalLayers) => {
return 0.3 + (0.5 * (totalLayers - layer)) / totalLayers;
};
switch (style) {
case "raised":
// Light on top/left, shadow on bottom/right
for (let i = 0; i < depth; i++) {
const alpha = getAlpha(i, depth);
this.drawBevel(
context,
width,
height,
i,
lightColor,
shadowColor,
alpha,
alpha,
);
}
break;
case "sunken":
// Shadow on top/left, light on bottom/right (swap colors)
for (let i = 0; i < depth; i++) {
const alpha = getAlpha(i, depth);
this.drawBevel(
context,
width,
height,
i,
shadowColor,
lightColor,
alpha,
alpha,
);
}
break;
case "ridge":
// Raised outer, sunken inner
const ridgeOuter = Math.ceil(depth / 2);
const ridgeInner = Math.floor(depth / 2);
// Outer raised bevel
for (let i = 0; i < ridgeOuter; i++) {
const alpha = getAlpha(i, ridgeOuter);
this.drawBevel(
context,
width,
height,
i,
lightColor,
shadowColor,
alpha,
alpha,
);
}
// Inner sunken bevel
for (let i = 0; i < ridgeInner; i++) {
const alpha = getAlpha(i, ridgeInner);
this.drawBevel(
context,
width,
height,
ridgeOuter + i,
shadowColor,
lightColor,
alpha,
alpha,
);
}
break;
case "groove":
// Sunken outer, raised inner (opposite of ridge)
const grooveOuter = Math.ceil(depth / 2);
const grooveInner = Math.floor(depth / 2);
// Outer sunken bevel
for (let i = 0; i < grooveOuter; i++) {
const alpha = getAlpha(i, grooveOuter);
this.drawBevel(
context,
width,
height,
i,
shadowColor,
lightColor,
alpha,
alpha,
);
}
// Inner raised bevel
for (let i = 0; i < grooveInner; i++) {
const alpha = getAlpha(i, grooveInner);
this.drawBevel(
context,
width,
height,
grooveOuter + i,
lightColor,
shadowColor,
alpha,
alpha,
);
}
break;
}
context.restore();
}
}
export function register(generator) {
generator.registerEffect(new EmbossEffect());
}

View file

@ -0,0 +1,187 @@
import { ButtonEffect } from "../effect-base.js";
/**
* Bouncing text animation effect
* Characters bounce individually with staggered timing
*/
export class BounceTextEffect extends ButtonEffect {
constructor(textLineNumber = 1) {
const suffix = textLineNumber === 1 ? "" : "2";
super({
id: `text-bounce${suffix}`,
name: `Bouncing Text ${textLineNumber}`,
type: textLineNumber === 1 ? "text" : "text2",
category: textLineNumber === 1 ? "Text Line 1" : "Text Line 2",
renderOrder: 9, // Between spin (8) and wave (10)
textLineNumber: textLineNumber,
});
this.textLineNumber = textLineNumber;
}
defineControls() {
const textLineNumber =
this.textLineNumber || this.config?.textLineNumber || 1;
const suffix = textLineNumber === 1 ? "" : "2";
return [
{
id: `animate-text-bounce${suffix}`,
type: "checkbox",
label: "Bounce Animation",
defaultValue: false,
},
{
id: `bounce-height${suffix}`,
type: "range",
label: "Bounce Height",
defaultValue: 5,
min: 2,
max: 15,
step: 1,
showWhen: `animate-text-bounce${suffix}`,
description: "How high characters bounce",
},
{
id: `bounce-speed${suffix}`,
type: "range",
label: "Bounce Speed",
defaultValue: 1.5,
min: 0.5,
max: 3,
step: 0.1,
showWhen: `animate-text-bounce${suffix}`,
description: "Speed of bounce animation",
},
{
id: `bounce-stagger${suffix}`,
type: "range",
label: "Stagger",
defaultValue: 0.15,
min: 0,
max: 0.5,
step: 0.05,
showWhen: `animate-text-bounce${suffix}`,
description: "Delay between character bounces",
},
];
}
isEnabled(controlValues) {
const suffix = this.textLineNumber === 1 ? "" : "2";
return controlValues[`animate-text-bounce${suffix}`] === true;
}
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
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;
if (!animState) return;
const bounceHeight = controlValues[`bounce-height${suffix}`] || 5;
const speed = controlValues[`bounce-speed${suffix}`] || 1.5;
const stagger = controlValues[`bounce-stagger${suffix}`] || 0.15;
const fontSize = controlValues[`font-size${suffix}`] || 14;
const fontFamily = controlValues[`font-family${suffix}`] || "Arial";
const fontWeight = controlValues[`text${suffix}-bold`] ? "bold" : "normal";
const fontStyle = controlValues[`text${suffix}-italic`] ? "italic" : "normal";
context.font = `${fontStyle} ${fontWeight} ${fontSize}px "${fontFamily}"`;
context.textAlign = "center";
context.textBaseline = "middle";
// Get text color
let fillStyle;
const colorType = controlValues[`text${suffix}-color-type`] || "solid";
if (colorType === "gradient") {
const color1 = controlValues[`text${suffix}-gradient-color1`] || "#ffffff";
const color2 = controlValues[`text${suffix}-gradient-color2`] || "#00ffff";
const angle = (controlValues[`text${suffix}-gradient-angle`] || 90) * (Math.PI / 180);
const centerX = renderData.centerX;
const centerY = renderData.centerY;
const x1 = centerX + Math.cos(angle) * 20;
const y1 = centerY + Math.sin(angle) * 20;
const x2 = centerX - Math.cos(angle) * 20;
const y2 = centerY - Math.sin(angle) * 20;
const gradient = context.createLinearGradient(x1, y1, x2, y2);
gradient.addColorStop(0, color1);
gradient.addColorStop(1, color2);
fillStyle = gradient;
} else {
fillStyle = controlValues[`text${suffix}-color`] || "#ffffff";
}
// Check for rainbow text effect
const rainbowActive = controlValues[`animate-text-rainbow${suffix}`];
// Calculate base position
const x = controlValues[`text${suffix}-x`] || 50;
const y = controlValues[`text${suffix}-y`] || 50;
const baseX = (x / 100) * renderData.width;
const baseY = (y / 100) * renderData.height;
// Split text into grapheme clusters (handles emojis properly)
let chars;
if (typeof Intl !== "undefined" && Intl.Segmenter) {
const segmenter = new Intl.Segmenter("en", { granularity: "grapheme" });
chars = Array.from(segmenter.segment(text), (s) => s.segment);
} else {
chars = [...text];
}
// Measure total text width for centering
const totalWidth = context.measureText(text).width;
let currentX = baseX - totalWidth / 2;
// Draw each character with bounce
for (let i = 0; i < chars.length; i++) {
const char = chars[i];
const charWidth = context.measureText(char).width;
const charCenterX = currentX + charWidth / 2;
// Calculate bounce offset for this character
// Use abs(sin) for bounce motion (always goes up, sharp at bottom)
const phase = animState.getPhase(speed) + i * stagger * Math.PI * 2;
const bounceOffset = -Math.abs(Math.sin(phase)) * bounceHeight;
context.save();
// Apply outline if enabled
if (controlValues[`text${suffix}-outline`]) {
context.strokeStyle = controlValues[`text${suffix}-outline-color`] || "#000000";
context.lineWidth = 2;
context.strokeText(char, charCenterX, baseY + bounceOffset);
}
// Handle rainbow color per character
if (rainbowActive) {
const rainbowSpeed = controlValues[`text-rainbow-speed${suffix}`] || 1;
const hue = ((animState.progress * rainbowSpeed * 360) + (i * 30)) % 360;
context.fillStyle = `hsl(${hue}, 80%, 60%)`;
} else {
context.fillStyle = fillStyle;
}
context.fillText(char, charCenterX, baseY + bounceOffset);
context.restore();
currentX += charWidth;
}
}
}
export function register(generator) {
generator.registerEffect(new BounceTextEffect(1));
generator.registerEffect(new BounceTextEffect(2));
}

View file

@ -0,0 +1,202 @@
import { ButtonEffect } from "../effect-base.js";
/**
* Neon glow text effect
* Pulsing outer glow - cyberpunk/cool site aesthetic
*/
export class TextGlowEffect extends ButtonEffect {
constructor(textLineNumber = 1) {
const suffix = textLineNumber === 1 ? "" : "2";
super({
id: `text-glow${suffix}`,
name: `Glow Text ${textLineNumber}`,
type: textLineNumber === 1 ? "text" : "text2",
category: textLineNumber === 1 ? "Text Line 1" : "Text Line 2",
renderOrder: 18, // Before standard text at 20
textLineNumber: textLineNumber,
});
this.textLineNumber = textLineNumber;
}
defineControls() {
const textLineNumber =
this.textLineNumber || this.config?.textLineNumber || 1;
const suffix = textLineNumber === 1 ? "" : "2";
return [
{
id: `animate-text-glow${suffix}`,
type: "checkbox",
label: "Neon Glow",
defaultValue: false,
},
{
id: `glow-color${suffix}`,
type: "color",
label: "Glow Color",
defaultValue: "#00ffff",
showWhen: `animate-text-glow${suffix}`,
description: "Color of the glow effect",
},
{
id: `glow-blur${suffix}`,
type: "range",
label: "Glow Size",
defaultValue: 8,
min: 2,
max: 20,
step: 1,
showWhen: `animate-text-glow${suffix}`,
description: "Size of the glow blur",
},
{
id: `glow-intensity${suffix}`,
type: "range",
label: "Intensity",
defaultValue: 1,
min: 0.5,
max: 2,
step: 0.1,
showWhen: `animate-text-glow${suffix}`,
description: "Glow brightness",
},
{
id: `glow-pulse${suffix}`,
type: "checkbox",
label: "Pulse Animation",
defaultValue: true,
showWhen: `animate-text-glow${suffix}`,
description: "Animate the glow",
},
{
id: `glow-pulse-speed${suffix}`,
type: "range",
label: "Pulse Speed",
defaultValue: 1,
min: 0.5,
max: 3,
step: 0.1,
showWhen: `animate-text-glow${suffix}`,
description: "Speed of pulse animation",
},
];
}
isEnabled(controlValues) {
const suffix = this.textLineNumber === 1 ? "" : "2";
return controlValues[`animate-text-glow${suffix}`] === true;
}
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
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;
const glowColor = controlValues[`glow-color${suffix}`] || "#00ffff";
const baseBlur = controlValues[`glow-blur${suffix}`] || 8;
const intensity = controlValues[`glow-intensity${suffix}`] || 1;
const pulse = controlValues[`glow-pulse${suffix}`] !== false;
const pulseSpeed = controlValues[`glow-pulse-speed${suffix}`] || 1;
// Calculate pulse multiplier
let pulseMultiplier = 1;
if (pulse && animState) {
const phase = animState.getPhase(pulseSpeed);
pulseMultiplier = 0.6 + Math.sin(phase) * 0.4;
}
const blur = baseBlur * pulseMultiplier;
// Get font settings
const fontSize = controlValues[`font-size${suffix}`] || 14;
const fontFamily = controlValues[`font-family${suffix}`] || "Arial";
const fontWeight = controlValues[`text${suffix}-bold`] ? "bold" : "normal";
const fontStyle = controlValues[`text${suffix}-italic`] ? "italic" : "normal";
context.font = `${fontStyle} ${fontWeight} ${fontSize}px "${fontFamily}"`;
context.textAlign = "center";
context.textBaseline = "middle";
// Get text color
let textColor;
const colorType = controlValues[`text${suffix}-color-type`] || "solid";
if (colorType === "gradient") {
textColor = controlValues[`text${suffix}-gradient-color1`] || "#ffffff";
} else {
textColor = controlValues[`text${suffix}-color`] || "#ffffff";
}
// Calculate position
const x = controlValues[`text${suffix}-x`] || 50;
const y = controlValues[`text${suffix}-y`] || 50;
const posX = (x / 100) * renderData.width;
const posY = (y / 100) * renderData.height;
context.save();
// Draw glow layers (multiple passes for stronger glow)
const passes = Math.ceil(intensity * 3);
for (let i = 0; i < passes; i++) {
context.shadowColor = glowColor;
context.shadowBlur = blur * (1 + i * 0.3);
context.shadowOffsetX = 0;
context.shadowOffsetY = 0;
context.fillStyle = glowColor;
context.globalAlpha = 0.3 * intensity * pulseMultiplier;
context.fillText(text, posX, posY);
}
// Draw outline if enabled
if (controlValues[`text${suffix}-outline`]) {
context.globalAlpha = 1;
context.shadowBlur = 0;
context.strokeStyle = controlValues[`text${suffix}-outline-color`] || "#000000";
context.lineWidth = 2;
context.strokeText(text, posX, posY);
}
// Draw main text with subtle glow
context.globalAlpha = 1;
context.shadowColor = glowColor;
context.shadowBlur = blur * 0.5;
// Handle gradient fill
if (colorType === "gradient") {
const color1 = controlValues[`text${suffix}-gradient-color1`] || "#ffffff";
const color2 = controlValues[`text${suffix}-gradient-color2`] || "#00ffff";
const angle = (controlValues[`text${suffix}-gradient-angle`] || 90) * (Math.PI / 180);
const textWidth = context.measureText(text).width;
const x1 = posX + Math.cos(angle) * textWidth * 0.5;
const y1 = posY + Math.sin(angle) * fontSize * 0.5;
const x2 = posX - Math.cos(angle) * textWidth * 0.5;
const y2 = posY - Math.sin(angle) * fontSize * 0.5;
const gradient = context.createLinearGradient(x1, y1, x2, y2);
gradient.addColorStop(0, color1);
gradient.addColorStop(1, color2);
context.fillStyle = gradient;
} else {
context.fillStyle = textColor;
}
context.fillText(text, posX, posY);
context.restore();
}
}
export function register(generator) {
generator.registerEffect(new TextGlowEffect(1));
generator.registerEffect(new TextGlowEffect(2));
}

View file

@ -151,8 +151,10 @@ export class StandardTextEffect extends ButtonEffect {
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}`]; const tickerActive = controlValues[`animate-text-ticker${suffix}`];
const bounceActive = controlValues[`animate-text-bounce${suffix}`];
const glowActive = controlValues[`animate-text-glow${suffix}`];
return text && text.trim() !== "" && !waveActive && !rainbowActive && !spinActive && !tickerActive; return text && text.trim() !== "" && !waveActive && !rainbowActive && !spinActive && !tickerActive && !bounceActive && !glowActive;
} }
apply(context, controlValues, animState, renderData) { apply(context, controlValues, animState, renderData) {

View file

@ -37,6 +37,11 @@ import * as noise from "./effects/noise.js";
import * as rotate from "./effects/rotate.js"; import * as rotate from "./effects/rotate.js";
import * as hologram from "./effects/hologram.js"; import * as hologram from "./effects/hologram.js";
import * as spotlight from "./effects/spotlight.js"; import * as spotlight from "./effects/spotlight.js";
import * as sparkle from "./effects/background-sparkle.js";
import * as textGlow from "./effects/text-glow.js";
import * as plasma from "./effects/background-plasma.js";
import * as emboss from "./effects/emboss.js";
import * as bounceText from "./effects/text-bounce.js";
/** /**
* Initialize the button generator application * Initialize the button generator application
@ -115,6 +120,11 @@ async function setupApp() {
rotate.register(generator); rotate.register(generator);
hologram.register(generator); hologram.register(generator);
spotlight.register(generator); spotlight.register(generator);
sparkle.register(generator);
textGlow.register(generator);
plasma.register(generator);
emboss.register(generator);
bounceText.register(generator);
console.log(`Registered ${generator.getAllEffects().length} effects`); console.log(`Registered ${generator.getAllEffects().length} effects`);