New effects, refactor
This commit is contained in:
parent
4ac45367e5
commit
c0d6bee9c3
14 changed files with 1620 additions and 215 deletions
|
|
@ -75,7 +75,6 @@
|
||||||
);
|
);
|
||||||
z-index: 90;
|
z-index: 90;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
content: "";
|
content: "";
|
||||||
|
|
@ -203,7 +202,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
label {
|
label {
|
||||||
display: block;
|
//display: block;
|
||||||
margin-top: 0.75rem;
|
margin-top: 0.75rem;
|
||||||
margin-bottom: 0.25rem;
|
margin-bottom: 0.25rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
|
@ -438,6 +437,7 @@
|
||||||
input[type="checkbox"] {
|
input[type="checkbox"] {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
margin-right: 0.25em;
|
||||||
}
|
}
|
||||||
|
|
||||||
span {
|
span {
|
||||||
|
|
@ -535,4 +535,45 @@
|
||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Custom tooltip styles
|
||||||
|
.control-tooltip {
|
||||||
|
position: fixed;
|
||||||
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
rgba(0, 120, 200, 0.98) 0%,
|
||||||
|
rgba(0, 100, 180, 0.98) 100%
|
||||||
|
);
|
||||||
|
color: #fff;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 10000;
|
||||||
|
max-width: 250px;
|
||||||
|
box-shadow:
|
||||||
|
0 0 20px rgba(0, 150, 255, 0.5),
|
||||||
|
0 4px 12px rgba(0, 0, 0, 0.4),
|
||||||
|
inset 0 0 20px rgba(0, 150, 255, 0.1);
|
||||||
|
border: 1px solid rgba(0, 150, 255, 0.6);
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.15s ease;
|
||||||
|
line-height: 1.4;
|
||||||
|
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
|
||||||
|
|
||||||
|
&.visible {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
bottom: 100%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
border: 6px solid transparent;
|
||||||
|
border-bottom-color: rgba(0, 120, 200, 0.98);
|
||||||
|
filter: drop-shadow(0 -2px 4px rgba(0, 0, 0, 0.2));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,10 +14,14 @@ This supports gif despite the basic `canvas` tag limitation courtesy of [gif.js]
|
||||||
|
|
||||||
Big thanks to [neonaut's 88x31 archive](https://neonaut.neocities.org/cyber/88x31) and everyone who made the buttons that appear there. You should check it out if you need inspiration for your button!
|
Big thanks to [neonaut's 88x31 archive](https://neonaut.neocities.org/cyber/88x31) and everyone who made the buttons that appear there. You should check it out if you need inspiration for your button!
|
||||||
|
|
||||||
|
**Important note:** Some effects and animations stack, some don't. Some work better with certain lengths of text or variables depending on text length. Experiment, see what happens.
|
||||||
|
|
||||||
{{< button-generator >}}
|
{{< button-generator >}}
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Changelog
|
### Changelog
|
||||||
|
|
||||||
- 08/01/2025 - Initial release.
|
- 07/01/2025 - Initial release.
|
||||||
|
- 08/01/2025 - Total refactor to be modular, added many more effects.
|
||||||
|
- 09/01/2025 - Rewrote the bevel inset and outset border code so they look a lot nicer at the corners. Updates throughout so that multibit (emoji!) characters should work.
|
||||||
|
|
|
||||||
191
static/js/button-generator/effects/background-aurora.js
Normal file
191
static/js/button-generator/effects/background-aurora.js
Normal file
|
|
@ -0,0 +1,191 @@
|
||||||
|
import { ButtonEffect } from "../effect-base.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Aurora/Plasma background effect
|
||||||
|
* Flowing organic color patterns using layered gradients
|
||||||
|
*/
|
||||||
|
export class AuroraEffect extends ButtonEffect {
|
||||||
|
constructor() {
|
||||||
|
super({
|
||||||
|
id: "bg-aurora",
|
||||||
|
name: "Aurora",
|
||||||
|
type: "general",
|
||||||
|
category: "Background Animations",
|
||||||
|
renderOrder: 55,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
defineControls() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: "animate-aurora",
|
||||||
|
type: "checkbox",
|
||||||
|
label: "Aurora Effect",
|
||||||
|
defaultValue: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "aurora-speed",
|
||||||
|
type: "range",
|
||||||
|
label: "Flow Speed",
|
||||||
|
defaultValue: 1,
|
||||||
|
min: 1,
|
||||||
|
max: 3,
|
||||||
|
step: 1,
|
||||||
|
showWhen: "animate-aurora",
|
||||||
|
description: "Speed of flowing colors",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "aurora-intensity",
|
||||||
|
type: "range",
|
||||||
|
label: "Intensity",
|
||||||
|
defaultValue: 0.6,
|
||||||
|
min: 0.2,
|
||||||
|
max: 1,
|
||||||
|
step: 0.1,
|
||||||
|
showWhen: "animate-aurora",
|
||||||
|
description: "Brightness and opacity",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "aurora-complexity",
|
||||||
|
type: "range",
|
||||||
|
label: "Complexity",
|
||||||
|
defaultValue: 3,
|
||||||
|
min: 2,
|
||||||
|
max: 6,
|
||||||
|
step: 1,
|
||||||
|
showWhen: "animate-aurora",
|
||||||
|
description: "Number of wave layers",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "aurora-color-scheme",
|
||||||
|
type: "select",
|
||||||
|
label: "Color Scheme",
|
||||||
|
defaultValue: "northern",
|
||||||
|
options: [
|
||||||
|
{ value: "northern", label: "Northern Lights" },
|
||||||
|
{ value: "purple", label: "Purple Dream" },
|
||||||
|
{ value: "fire", label: "Fire" },
|
||||||
|
{ value: "ocean", label: "Ocean" },
|
||||||
|
{ value: "rainbow", label: "Rainbow" },
|
||||||
|
],
|
||||||
|
showWhen: "animate-aurora",
|
||||||
|
description: "Color palette",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
isEnabled(controlValues) {
|
||||||
|
return controlValues["animate-aurora"] === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
getColorScheme(scheme, hue) {
|
||||||
|
switch (scheme) {
|
||||||
|
case "northern":
|
||||||
|
return [
|
||||||
|
{ h: 120, s: 70, l: 50 }, // Green
|
||||||
|
{ h: 160, s: 70, l: 50 }, // Teal
|
||||||
|
{ h: 200, s: 70, l: 50 }, // Blue
|
||||||
|
];
|
||||||
|
case "purple":
|
||||||
|
return [
|
||||||
|
{ h: 270, s: 70, l: 50 }, // Purple
|
||||||
|
{ h: 300, s: 70, l: 50 }, // Magenta
|
||||||
|
{ h: 330, s: 70, l: 50 }, // Pink
|
||||||
|
];
|
||||||
|
case "fire":
|
||||||
|
return [
|
||||||
|
{ h: 0, s: 80, l: 50 }, // Red
|
||||||
|
{ h: 30, s: 80, l: 50 }, // Orange
|
||||||
|
{ h: 50, s: 80, l: 50 }, // Yellow-Orange
|
||||||
|
];
|
||||||
|
case "ocean":
|
||||||
|
return [
|
||||||
|
{ h: 180, s: 70, l: 50 }, // Cyan
|
||||||
|
{ h: 200, s: 70, l: 50 }, // Light Blue
|
||||||
|
{ h: 220, s: 70, l: 50 }, // Blue
|
||||||
|
];
|
||||||
|
case "rainbow":
|
||||||
|
return [
|
||||||
|
{ h: (hue + 0) % 360, s: 70, l: 50 },
|
||||||
|
{ h: (hue + 120) % 360, s: 70, l: 50 },
|
||||||
|
{ h: (hue + 240) % 360, s: 70, l: 50 },
|
||||||
|
];
|
||||||
|
default:
|
||||||
|
return [
|
||||||
|
{ h: 120, s: 70, l: 50 },
|
||||||
|
{ h: 180, s: 70, l: 50 },
|
||||||
|
{ h: 240, s: 70, l: 50 },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
apply(context, controlValues, animState, renderData) {
|
||||||
|
if (!animState) return;
|
||||||
|
|
||||||
|
const speed = controlValues["aurora-speed"] || 1;
|
||||||
|
const intensity = controlValues["aurora-intensity"] || 0.6;
|
||||||
|
const complexity = controlValues["aurora-complexity"] || 3;
|
||||||
|
const colorScheme = controlValues["aurora-color-scheme"] || "northern";
|
||||||
|
|
||||||
|
const time = animState.getPhase(speed);
|
||||||
|
|
||||||
|
// Create flowing hue shift that loops properly (only used for rainbow scheme)
|
||||||
|
// Convert phase (0 to 2π) to hue degrees (0 to 360)
|
||||||
|
const hueShift = (time / (Math.PI * 2)) * 360;
|
||||||
|
const colors = this.getColorScheme(colorScheme, hueShift);
|
||||||
|
|
||||||
|
// Draw multiple overlapping gradients to create aurora effect
|
||||||
|
context.globalCompositeOperation = "screen"; // Blend mode for aurora effect
|
||||||
|
|
||||||
|
for (let i = 0; i < complexity; i++) {
|
||||||
|
const phase = time + i * ((Math.PI * 2) / complexity);
|
||||||
|
|
||||||
|
// Calculate wave positions
|
||||||
|
const wave1X =
|
||||||
|
renderData.centerX + Math.sin(phase) * renderData.width * 0.5;
|
||||||
|
const wave1Y =
|
||||||
|
renderData.centerY + Math.cos(phase * 1.3) * renderData.height * 0.5;
|
||||||
|
|
||||||
|
// Create radial gradient
|
||||||
|
const gradient = context.createRadialGradient(
|
||||||
|
wave1X,
|
||||||
|
wave1Y,
|
||||||
|
0,
|
||||||
|
wave1X,
|
||||||
|
wave1Y,
|
||||||
|
renderData.width * 0.8,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Pick color based on wave index
|
||||||
|
const colorIdx = i % colors.length;
|
||||||
|
const color = colors[colorIdx];
|
||||||
|
|
||||||
|
const baseOpacity = intensity * 0.3;
|
||||||
|
|
||||||
|
// Rainbow scheme already has hueShift applied in getColorScheme
|
||||||
|
// Other schemes use their fixed colors
|
||||||
|
gradient.addColorStop(
|
||||||
|
0,
|
||||||
|
`hsla(${color.h}, ${color.s}%, ${color.l}%, ${baseOpacity})`,
|
||||||
|
);
|
||||||
|
gradient.addColorStop(
|
||||||
|
0.5,
|
||||||
|
`hsla(${color.h}, ${color.s}%, ${color.l}%, ${baseOpacity * 0.5})`,
|
||||||
|
);
|
||||||
|
gradient.addColorStop(
|
||||||
|
1,
|
||||||
|
`hsla(${color.h}, ${color.s}%, ${color.l}%, 0)`,
|
||||||
|
);
|
||||||
|
|
||||||
|
context.fillStyle = gradient;
|
||||||
|
context.fillRect(0, 0, renderData.width, renderData.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset composite operation
|
||||||
|
context.globalCompositeOperation = "source-over";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function register(generator) {
|
||||||
|
generator.registerEffect(new AuroraEffect());
|
||||||
|
}
|
||||||
178
static/js/button-generator/effects/background-bubbles.js
Normal file
178
static/js/button-generator/effects/background-bubbles.js
Normal file
|
|
@ -0,0 +1,178 @@
|
||||||
|
import { ButtonEffect } from "../effect-base.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bubbles rising background effect
|
||||||
|
* Floating bubbles that rise with drift
|
||||||
|
*/
|
||||||
|
export class BubblesEffect extends ButtonEffect {
|
||||||
|
constructor() {
|
||||||
|
super({
|
||||||
|
id: "bg-bubbles",
|
||||||
|
name: "Bubbles",
|
||||||
|
type: "general",
|
||||||
|
category: "Background Animations",
|
||||||
|
renderOrder: 55,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.bubbles = [];
|
||||||
|
this.initialized = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
defineControls() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: "animate-bubbles",
|
||||||
|
type: "checkbox",
|
||||||
|
label: "Bubbles Effect",
|
||||||
|
defaultValue: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "bubble-count",
|
||||||
|
type: "range",
|
||||||
|
label: "Bubble Count",
|
||||||
|
defaultValue: 15,
|
||||||
|
min: 5,
|
||||||
|
max: 40,
|
||||||
|
step: 1,
|
||||||
|
showWhen: "animate-bubbles",
|
||||||
|
description: "Number of bubbles",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "bubble-speed",
|
||||||
|
type: "range",
|
||||||
|
label: "Rise Speed",
|
||||||
|
defaultValue: 1,
|
||||||
|
min: 0.3,
|
||||||
|
max: 3,
|
||||||
|
step: 0.1,
|
||||||
|
showWhen: "animate-bubbles",
|
||||||
|
description: "Speed of rising bubbles",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "bubble-drift",
|
||||||
|
type: "range",
|
||||||
|
label: "Drift Amount",
|
||||||
|
defaultValue: 1,
|
||||||
|
min: 0,
|
||||||
|
max: 3,
|
||||||
|
step: 0.1,
|
||||||
|
showWhen: "animate-bubbles",
|
||||||
|
description: "Side-to-side drift",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "bubble-color",
|
||||||
|
type: "color",
|
||||||
|
label: "Bubble Color",
|
||||||
|
defaultValue: "#4da6ff",
|
||||||
|
showWhen: "animate-bubbles",
|
||||||
|
description: "Color of bubbles",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
isEnabled(controlValues) {
|
||||||
|
return controlValues["animate-bubbles"] === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
apply(context, controlValues, animState, renderData) {
|
||||||
|
if (!animState) return;
|
||||||
|
|
||||||
|
const count = controlValues["bubble-count"] || 15;
|
||||||
|
const speed = controlValues["bubble-speed"] || 1;
|
||||||
|
const drift = controlValues["bubble-drift"] || 1;
|
||||||
|
const bubbleColor = controlValues["bubble-color"] || "#4da6ff";
|
||||||
|
|
||||||
|
// Initialize bubbles on first frame or count change
|
||||||
|
if (!this.initialized || this.bubbles.length !== count) {
|
||||||
|
this.bubbles = [];
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
this.bubbles.push({
|
||||||
|
x: Math.random() * renderData.width,
|
||||||
|
startY: Math.random(), // Store as 0-1 ratio instead of pixel value
|
||||||
|
size: 3 + Math.random() * 8,
|
||||||
|
speedMultiplier: 0.5 + Math.random() * 1,
|
||||||
|
driftPhase: Math.random() * Math.PI * 2,
|
||||||
|
driftSpeed: 0.5 + Math.random() * 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.initialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse color for gradient
|
||||||
|
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: 77, g: 166, b: 255 };
|
||||||
|
};
|
||||||
|
|
||||||
|
const rgb = hexToRgb(bubbleColor);
|
||||||
|
|
||||||
|
// Draw bubbles
|
||||||
|
this.bubbles.forEach((bubble) => {
|
||||||
|
// Calculate Y position based on animation progress for perfect looping
|
||||||
|
// Each bubble has a different starting offset and speed
|
||||||
|
const bubbleProgress =
|
||||||
|
(animState.progress * speed * bubble.speedMultiplier + bubble.startY) %
|
||||||
|
1;
|
||||||
|
|
||||||
|
// Convert to pixel position (bubbles rise from bottom to top)
|
||||||
|
const bubbleY =
|
||||||
|
renderData.height + bubble.size - bubbleProgress * (renderData.height + bubble.size * 2);
|
||||||
|
|
||||||
|
// Drift side to side
|
||||||
|
const driftOffset =
|
||||||
|
Math.sin(
|
||||||
|
animState.getPhase(bubble.driftSpeed * 0.5) + bubble.driftPhase
|
||||||
|
) *
|
||||||
|
drift *
|
||||||
|
2;
|
||||||
|
const currentX = bubble.x + driftOffset;
|
||||||
|
|
||||||
|
// Draw bubble with gradient for 3D effect
|
||||||
|
const gradient = context.createRadialGradient(
|
||||||
|
currentX - bubble.size * 0.3,
|
||||||
|
bubbleY - bubble.size * 0.3,
|
||||||
|
0,
|
||||||
|
currentX,
|
||||||
|
bubbleY,
|
||||||
|
bubble.size
|
||||||
|
);
|
||||||
|
|
||||||
|
gradient.addColorStop(
|
||||||
|
0,
|
||||||
|
`rgba(${rgb.r + 40}, ${rgb.g + 40}, ${rgb.b + 40}, 0.6)`
|
||||||
|
);
|
||||||
|
gradient.addColorStop(
|
||||||
|
0.6,
|
||||||
|
`rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.4)`
|
||||||
|
);
|
||||||
|
gradient.addColorStop(1, `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.2)`);
|
||||||
|
|
||||||
|
context.fillStyle = gradient;
|
||||||
|
context.beginPath();
|
||||||
|
context.arc(currentX, bubbleY, bubble.size, 0, Math.PI * 2);
|
||||||
|
context.fill();
|
||||||
|
|
||||||
|
// Add highlight
|
||||||
|
context.fillStyle = "rgba(255, 255, 255, 0.4)";
|
||||||
|
context.beginPath();
|
||||||
|
context.arc(
|
||||||
|
currentX - bubble.size * 0.3,
|
||||||
|
bubbleY - bubble.size * 0.3,
|
||||||
|
bubble.size * 0.3,
|
||||||
|
0,
|
||||||
|
Math.PI * 2
|
||||||
|
);
|
||||||
|
context.fill();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function register(generator) {
|
||||||
|
generator.registerEffect(new BubblesEffect());
|
||||||
|
}
|
||||||
216
static/js/button-generator/effects/background-fire.js
Normal file
216
static/js/button-generator/effects/background-fire.js
Normal file
|
|
@ -0,0 +1,216 @@
|
||||||
|
import { ButtonEffect } from "../effect-base.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fire background effect
|
||||||
|
* Animated flames rising from bottom using particles
|
||||||
|
*/
|
||||||
|
export class FireEffect extends ButtonEffect {
|
||||||
|
constructor() {
|
||||||
|
super({
|
||||||
|
id: "bg-fire",
|
||||||
|
name: "Fire",
|
||||||
|
type: "general",
|
||||||
|
category: "Background Animations",
|
||||||
|
renderOrder: 55,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.particles = [];
|
||||||
|
this.initialized = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
defineControls() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: "animate-fire",
|
||||||
|
type: "checkbox",
|
||||||
|
label: "Fire Effect",
|
||||||
|
defaultValue: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "fire-intensity",
|
||||||
|
type: "range",
|
||||||
|
label: "Intensity",
|
||||||
|
defaultValue: 50,
|
||||||
|
min: 20,
|
||||||
|
max: 100,
|
||||||
|
step: 5,
|
||||||
|
showWhen: "animate-fire",
|
||||||
|
description: "Number of flame particles",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "fire-height",
|
||||||
|
type: "range",
|
||||||
|
label: "Flame Height",
|
||||||
|
defaultValue: 0.6,
|
||||||
|
min: 0.3,
|
||||||
|
max: 1,
|
||||||
|
step: 0.1,
|
||||||
|
showWhen: "animate-fire",
|
||||||
|
description: "How high flames reach",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "fire-speed",
|
||||||
|
type: "range",
|
||||||
|
label: "Speed",
|
||||||
|
defaultValue: 1,
|
||||||
|
min: 0.3,
|
||||||
|
max: 3,
|
||||||
|
step: 0.1,
|
||||||
|
showWhen: "animate-fire",
|
||||||
|
description: "Speed of rising flames",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "fire-color-scheme",
|
||||||
|
type: "select",
|
||||||
|
label: "Color Scheme",
|
||||||
|
defaultValue: "normal",
|
||||||
|
options: [
|
||||||
|
{ value: "normal", label: "Normal Fire" },
|
||||||
|
{ value: "blue", label: "Blue Flame" },
|
||||||
|
{ value: "green", label: "Green Flame" },
|
||||||
|
{ value: "purple", label: "Purple Flame" },
|
||||||
|
{ value: "white", label: "White Hot" },
|
||||||
|
],
|
||||||
|
showWhen: "animate-fire",
|
||||||
|
description: "Flame color",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
isEnabled(controlValues) {
|
||||||
|
return controlValues["animate-fire"] === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
getFireColors(scheme) {
|
||||||
|
switch (scheme) {
|
||||||
|
case "normal":
|
||||||
|
return [
|
||||||
|
{ r: 255, g: 60, b: 0 }, // Red-orange
|
||||||
|
{ r: 255, g: 140, b: 0 }, // Orange
|
||||||
|
{ r: 255, g: 200, b: 0 }, // Yellow
|
||||||
|
];
|
||||||
|
case "blue":
|
||||||
|
return [
|
||||||
|
{ r: 0, g: 100, b: 255 }, // Blue
|
||||||
|
{ r: 100, g: 180, b: 255 }, // Light blue
|
||||||
|
{ r: 200, g: 230, b: 255 }, // Very light blue
|
||||||
|
];
|
||||||
|
case "green":
|
||||||
|
return [
|
||||||
|
{ r: 0, g: 200, b: 50 }, // Green
|
||||||
|
{ r: 100, g: 255, b: 100 }, // Light green
|
||||||
|
{ r: 200, g: 255, b: 150 }, // Very light green
|
||||||
|
];
|
||||||
|
case "purple":
|
||||||
|
return [
|
||||||
|
{ r: 150, g: 0, b: 255 }, // Purple
|
||||||
|
{ r: 200, g: 100, b: 255 }, // Light purple
|
||||||
|
{ r: 230, g: 180, b: 255 }, // Very light purple
|
||||||
|
];
|
||||||
|
case "white":
|
||||||
|
return [
|
||||||
|
{ r: 255, g: 200, b: 150 }, // Warm white
|
||||||
|
{ r: 255, g: 240, b: 200 }, // Light white
|
||||||
|
{ r: 255, g: 255, b: 255 }, // Pure white
|
||||||
|
];
|
||||||
|
default:
|
||||||
|
return [
|
||||||
|
{ r: 255, g: 60, b: 0 },
|
||||||
|
{ r: 255, g: 140, b: 0 },
|
||||||
|
{ r: 255, g: 200, b: 0 },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
apply(context, controlValues, animState, renderData) {
|
||||||
|
if (!animState) return;
|
||||||
|
|
||||||
|
const intensity = controlValues["fire-intensity"] || 50;
|
||||||
|
const height = controlValues["fire-height"] || 0.6;
|
||||||
|
const speed = controlValues["fire-speed"] || 1;
|
||||||
|
const colorScheme = controlValues["fire-color-scheme"] || "normal";
|
||||||
|
|
||||||
|
const colors = this.getFireColors(colorScheme);
|
||||||
|
const maxHeight = renderData.height * height;
|
||||||
|
|
||||||
|
// Spawn new particles at the bottom
|
||||||
|
for (let i = 0; i < intensity / 10; i++) {
|
||||||
|
this.particles.push({
|
||||||
|
x: Math.random() * renderData.width,
|
||||||
|
y: renderData.height,
|
||||||
|
vx: (Math.random() - 0.5) * 1.5,
|
||||||
|
vy: -(2 + Math.random() * 3) * speed,
|
||||||
|
size: 2 + Math.random() * 6,
|
||||||
|
life: 1.0,
|
||||||
|
colorIndex: Math.random(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update and draw particles
|
||||||
|
this.particles = this.particles.filter((particle) => {
|
||||||
|
// Update position
|
||||||
|
particle.x += particle.vx;
|
||||||
|
particle.y += particle.vy;
|
||||||
|
|
||||||
|
// Add some turbulence
|
||||||
|
particle.vx += (Math.random() - 0.5) * 0.2;
|
||||||
|
particle.vy *= 0.98; // Slow down as they rise
|
||||||
|
|
||||||
|
// Fade out based on height and time
|
||||||
|
const heightRatio =
|
||||||
|
(renderData.height - particle.y) / renderData.height;
|
||||||
|
particle.life -= 0.015;
|
||||||
|
|
||||||
|
if (particle.life > 0 && particle.y > renderData.height - maxHeight) {
|
||||||
|
// Choose color based on life (hotter at bottom, cooler at top)
|
||||||
|
const colorProgress = 1 - particle.life;
|
||||||
|
const colorIdx = Math.floor(colorProgress * (colors.length - 1));
|
||||||
|
const colorBlend = (colorProgress * (colors.length - 1)) % 1;
|
||||||
|
|
||||||
|
const c1 = colors[Math.min(colorIdx, colors.length - 1)];
|
||||||
|
const c2 = colors[Math.min(colorIdx + 1, colors.length - 1)];
|
||||||
|
|
||||||
|
const r = Math.floor(c1.r + (c2.r - c1.r) * colorBlend);
|
||||||
|
const g = Math.floor(c1.g + (c2.g - c1.g) * colorBlend);
|
||||||
|
const b = Math.floor(c1.b + (c2.b - c1.b) * colorBlend);
|
||||||
|
|
||||||
|
// Draw particle with gradient
|
||||||
|
const gradient = context.createRadialGradient(
|
||||||
|
particle.x,
|
||||||
|
particle.y,
|
||||||
|
0,
|
||||||
|
particle.x,
|
||||||
|
particle.y,
|
||||||
|
particle.size
|
||||||
|
);
|
||||||
|
|
||||||
|
gradient.addColorStop(
|
||||||
|
0,
|
||||||
|
`rgba(${r}, ${g}, ${b}, ${particle.life * 0.8})`
|
||||||
|
);
|
||||||
|
gradient.addColorStop(
|
||||||
|
0.5,
|
||||||
|
`rgba(${r}, ${g}, ${b}, ${particle.life * 0.5})`
|
||||||
|
);
|
||||||
|
gradient.addColorStop(1, `rgba(${r}, ${g}, ${b}, 0)`);
|
||||||
|
|
||||||
|
context.fillStyle = gradient;
|
||||||
|
context.beginPath();
|
||||||
|
context.arc(particle.x, particle.y, particle.size, 0, Math.PI * 2);
|
||||||
|
context.fill();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Limit particle count
|
||||||
|
if (this.particles.length > intensity * 5) {
|
||||||
|
this.particles = this.particles.slice(-intensity * 5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function register(generator) {
|
||||||
|
generator.registerEffect(new FireEffect());
|
||||||
|
}
|
||||||
166
static/js/button-generator/effects/background-starfield.js
Normal file
166
static/js/button-generator/effects/background-starfield.js
Normal file
|
|
@ -0,0 +1,166 @@
|
||||||
|
import { ButtonEffect } from "../effect-base.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starfield background effect
|
||||||
|
* Twinkling stars with optional shooting stars
|
||||||
|
*/
|
||||||
|
export class StarfieldEffect extends ButtonEffect {
|
||||||
|
constructor() {
|
||||||
|
super({
|
||||||
|
id: "bg-starfield",
|
||||||
|
name: "Starfield",
|
||||||
|
type: "general",
|
||||||
|
category: "Background Animations",
|
||||||
|
renderOrder: 55,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.stars = [];
|
||||||
|
this.shootingStars = [];
|
||||||
|
this.initialized = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
defineControls() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: "animate-starfield",
|
||||||
|
type: "checkbox",
|
||||||
|
label: "Starfield Effect",
|
||||||
|
description:
|
||||||
|
"This might look a bit different when exported, work in progress!",
|
||||||
|
defaultValue: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "star-density",
|
||||||
|
type: "range",
|
||||||
|
label: "Star Density",
|
||||||
|
defaultValue: 30,
|
||||||
|
min: 10,
|
||||||
|
max: 80,
|
||||||
|
step: 5,
|
||||||
|
showWhen: "animate-starfield",
|
||||||
|
description: "Number of stars",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "star-twinkle-speed",
|
||||||
|
type: "range",
|
||||||
|
label: "Twinkle Speed",
|
||||||
|
defaultValue: 1,
|
||||||
|
min: 0.1,
|
||||||
|
max: 3,
|
||||||
|
step: 0.1,
|
||||||
|
showWhen: "animate-starfield",
|
||||||
|
description: "Speed of twinkling",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "star-shooting-enabled",
|
||||||
|
type: "checkbox",
|
||||||
|
label: "Shooting Stars",
|
||||||
|
defaultValue: true,
|
||||||
|
showWhen: "animate-starfield",
|
||||||
|
description: "Enable shooting stars",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "star-color",
|
||||||
|
type: "color",
|
||||||
|
label: "Star Color",
|
||||||
|
defaultValue: "#ffffff",
|
||||||
|
showWhen: "animate-starfield",
|
||||||
|
description: "Color of stars",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
isEnabled(controlValues) {
|
||||||
|
return controlValues["animate-starfield"] === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
apply(context, controlValues, animState, renderData) {
|
||||||
|
if (!animState) return;
|
||||||
|
|
||||||
|
const density = controlValues["star-density"] || 30;
|
||||||
|
const twinkleSpeed = controlValues["star-twinkle-speed"] || 1;
|
||||||
|
const shootingEnabled = controlValues["star-shooting-enabled"] !== false;
|
||||||
|
const starColor = controlValues["star-color"] || "#ffffff";
|
||||||
|
|
||||||
|
// Initialize stars on first frame or density change
|
||||||
|
if (!this.initialized || this.stars.length !== density) {
|
||||||
|
this.stars = [];
|
||||||
|
for (let i = 0; i < density; i++) {
|
||||||
|
this.stars.push({
|
||||||
|
x: Math.random() * renderData.width,
|
||||||
|
y: Math.random() * renderData.height,
|
||||||
|
size: 0.5 + Math.random() * 1.5,
|
||||||
|
twinkleOffset: Math.random() * Math.PI * 2,
|
||||||
|
twinkleSpeed: 0.5 + Math.random() * 1.5,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.initialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw twinkling stars
|
||||||
|
this.stars.forEach((star) => {
|
||||||
|
const twinkle =
|
||||||
|
Math.sin(
|
||||||
|
animState.getPhase(twinkleSpeed * star.twinkleSpeed) +
|
||||||
|
star.twinkleOffset,
|
||||||
|
) *
|
||||||
|
0.5 +
|
||||||
|
0.5;
|
||||||
|
const opacity = 0.3 + twinkle * 0.7;
|
||||||
|
|
||||||
|
context.fillStyle = starColor;
|
||||||
|
context.globalAlpha = opacity;
|
||||||
|
context.beginPath();
|
||||||
|
context.arc(star.x, star.y, star.size, 0, Math.PI * 2);
|
||||||
|
context.fill();
|
||||||
|
context.globalAlpha = 1.0;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Shooting stars
|
||||||
|
if (shootingEnabled) {
|
||||||
|
// Randomly spawn shooting stars
|
||||||
|
if (Math.random() < 0.02 && this.shootingStars.length < 3) {
|
||||||
|
this.shootingStars.push({
|
||||||
|
x: Math.random() * renderData.width,
|
||||||
|
y: -10,
|
||||||
|
vx: (Math.random() - 0.5) * 2,
|
||||||
|
vy: 3 + Math.random() * 2,
|
||||||
|
life: 1.0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update and draw shooting stars
|
||||||
|
this.shootingStars = this.shootingStars.filter((star) => {
|
||||||
|
star.x += star.vx;
|
||||||
|
star.y += star.vy;
|
||||||
|
star.life -= 0.02;
|
||||||
|
|
||||||
|
if (star.life > 0) {
|
||||||
|
// Draw shooting star trail
|
||||||
|
const gradient = context.createLinearGradient(
|
||||||
|
star.x,
|
||||||
|
star.y,
|
||||||
|
star.x - star.vx * 5,
|
||||||
|
star.y - star.vy * 5,
|
||||||
|
);
|
||||||
|
gradient.addColorStop(0, `rgba(255, 255, 255, ${star.life * 0.8})`);
|
||||||
|
gradient.addColorStop(1, "rgba(255, 255, 255, 0)");
|
||||||
|
|
||||||
|
context.strokeStyle = gradient;
|
||||||
|
context.lineWidth = 2;
|
||||||
|
context.beginPath();
|
||||||
|
context.moveTo(star.x, star.y);
|
||||||
|
context.lineTo(star.x - star.vx * 5, star.y - star.vy * 5);
|
||||||
|
context.stroke();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function register(generator) {
|
||||||
|
generator.registerEffect(new StarfieldEffect());
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { ButtonEffect } from '../effect-base.js';
|
import { ButtonEffect } from "../effect-base.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Border effect
|
* Border effect
|
||||||
|
|
@ -7,65 +7,107 @@ import { ButtonEffect } from '../effect-base.js';
|
||||||
export class BorderEffect extends ButtonEffect {
|
export class BorderEffect extends ButtonEffect {
|
||||||
constructor() {
|
constructor() {
|
||||||
super({
|
super({
|
||||||
id: 'border',
|
id: "border",
|
||||||
name: 'Border',
|
name: "Border",
|
||||||
type: 'border',
|
type: "border",
|
||||||
category: 'Border',
|
category: "Border",
|
||||||
renderOrder: 10
|
renderOrder: 10,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
defineControls() {
|
defineControls() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
id: 'border-width',
|
id: "border-width",
|
||||||
type: 'range',
|
type: "range",
|
||||||
label: 'Border Width',
|
label: "Border Width",
|
||||||
defaultValue: 2,
|
defaultValue: 2,
|
||||||
min: 0,
|
min: 0,
|
||||||
max: 5,
|
max: 5,
|
||||||
step: 1,
|
step: 1,
|
||||||
description: 'Width of border in pixels'
|
description: "Width of border in pixels",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'border-color',
|
id: "border-color",
|
||||||
type: 'color',
|
type: "color",
|
||||||
label: 'Border Color',
|
label: "Border Color",
|
||||||
defaultValue: '#000000'
|
defaultValue: "#000000",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'border-style',
|
id: "border-style",
|
||||||
type: 'select',
|
type: "select",
|
||||||
label: 'Border Style',
|
label: "Border Style",
|
||||||
defaultValue: 'solid',
|
defaultValue: "solid",
|
||||||
options: [
|
options: [
|
||||||
{ value: 'solid', label: 'Solid' },
|
{ value: "solid", label: "Solid" },
|
||||||
{ value: 'inset', label: 'Inset (3D)' },
|
{ value: "dashed", label: "Dashed" },
|
||||||
{ value: 'outset', label: 'Outset (3D)' },
|
{ value: "dotted", label: "Dotted" },
|
||||||
{ value: 'ridge', label: 'Ridge' }
|
{ value: "double", label: "Double" },
|
||||||
]
|
{ value: "inset", label: "Inset (3D)" },
|
||||||
}
|
{ value: "outset", label: "Outset (3D)" },
|
||||||
|
{ value: "ridge", label: "Ridge" },
|
||||||
|
{ value: "rainbow", label: "Rainbow (Animated)" },
|
||||||
|
{ value: "marching-ants", label: "Marching Ants" },
|
||||||
|
{ value: "checkerboard", label: "Checkerboard" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "border-rainbow-speed",
|
||||||
|
type: "range",
|
||||||
|
label: "Rainbow Speed",
|
||||||
|
defaultValue: 1,
|
||||||
|
min: 1,
|
||||||
|
max: 3,
|
||||||
|
step: 1,
|
||||||
|
showWhen: "border-style",
|
||||||
|
description: "Speed of rainbow animation",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "border-march-speed",
|
||||||
|
type: "range",
|
||||||
|
label: "March Speed",
|
||||||
|
defaultValue: 1,
|
||||||
|
min: 1,
|
||||||
|
max: 3,
|
||||||
|
step: 1,
|
||||||
|
showWhen: "border-style",
|
||||||
|
description: "Speed of marching animation",
|
||||||
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
isEnabled(controlValues) {
|
isEnabled(controlValues) {
|
||||||
const width = controlValues['border-width'] || 0;
|
const width = controlValues["border-width"] || 0;
|
||||||
return width > 0;
|
return width > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
apply(context, controlValues, animState, renderData) {
|
apply(context, controlValues, animState, renderData) {
|
||||||
const width = controlValues['border-width'] || 0;
|
const width = controlValues["border-width"] || 0;
|
||||||
if (width === 0) return;
|
if (width === 0) return;
|
||||||
|
|
||||||
const color = controlValues['border-color'] || '#000000';
|
const color = controlValues["border-color"] || "#000000";
|
||||||
const style = controlValues['border-style'] || 'solid';
|
const style = controlValues["border-style"] || "solid";
|
||||||
|
|
||||||
if (style === 'solid') {
|
if (style === "solid") {
|
||||||
this.drawSolidBorder(context, width, color, renderData);
|
this.drawSolidBorder(context, width, color, renderData);
|
||||||
} else if (style === 'inset' || style === 'outset') {
|
} else if (style === "dashed") {
|
||||||
this.draw3DBorder(context, width, style === 'outset', renderData);
|
this.drawDashedBorder(context, width, color, renderData);
|
||||||
} else if (style === 'ridge') {
|
} else if (style === "dotted") {
|
||||||
|
this.drawDottedBorder(context, width, color, renderData);
|
||||||
|
} else if (style === "double") {
|
||||||
|
this.drawDoubleBorder(context, width, color, renderData);
|
||||||
|
} else if (style === "inset" || style === "outset") {
|
||||||
|
this.draw3DBorder(context, width, color, style === "outset", renderData);
|
||||||
|
} else if (style === "ridge") {
|
||||||
this.drawRidgeBorder(context, width, renderData);
|
this.drawRidgeBorder(context, width, renderData);
|
||||||
|
} else if (style === "rainbow") {
|
||||||
|
const speed = controlValues["border-rainbow-speed"] || 1;
|
||||||
|
this.drawRainbowBorder(context, width, animState, speed, renderData);
|
||||||
|
} else if (style === "marching-ants") {
|
||||||
|
const speed = controlValues["border-march-speed"] || 1;
|
||||||
|
this.drawMarchingAntsBorder(context, width, animState, speed, renderData);
|
||||||
|
} else if (style === "checkerboard") {
|
||||||
|
this.drawCheckerboardBorder(context, width, color, renderData);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -79,33 +121,65 @@ export class BorderEffect extends ButtonEffect {
|
||||||
width / 2,
|
width / 2,
|
||||||
width / 2,
|
width / 2,
|
||||||
renderData.width - width,
|
renderData.width - width,
|
||||||
renderData.height - width
|
renderData.height - width,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Draw 3D inset/outset border
|
* Draw 3D inset/outset border
|
||||||
*/
|
*/
|
||||||
draw3DBorder(context, width, isOutset, renderData) {
|
draw3DBorder(context, width, color, isOutset, renderData) {
|
||||||
const lightColor = isOutset ? '#ffffff' : '#000000';
|
const w = renderData.width;
|
||||||
const darkColor = isOutset ? '#000000' : '#ffffff';
|
const h = renderData.height;
|
||||||
|
const t = width;
|
||||||
|
|
||||||
// Top and left (light)
|
const normalized = color.toLowerCase();
|
||||||
context.strokeStyle = lightColor;
|
const isPureBlack = normalized === "#000000";
|
||||||
context.lineWidth = width;
|
const isPureWhite = normalized === "#ffffff";
|
||||||
context.beginPath();
|
|
||||||
context.moveTo(0, renderData.height);
|
|
||||||
context.lineTo(0, 0);
|
|
||||||
context.lineTo(renderData.width, 0);
|
|
||||||
context.stroke();
|
|
||||||
|
|
||||||
// Bottom and right (dark)
|
let lightColor;
|
||||||
context.strokeStyle = darkColor;
|
let darkColor;
|
||||||
context.beginPath();
|
|
||||||
context.moveTo(renderData.width, 0);
|
if (isPureBlack || isPureWhite) {
|
||||||
context.lineTo(renderData.width, renderData.height);
|
lightColor = isOutset ? "#ffffff" : "#000000";
|
||||||
context.lineTo(0, renderData.height);
|
darkColor = isOutset ? "#000000" : "#ffffff";
|
||||||
context.stroke();
|
} else {
|
||||||
|
const lighter = this.adjustColor(color, 0.25);
|
||||||
|
const darker = this.adjustColor(color, -0.25);
|
||||||
|
|
||||||
|
lightColor = isOutset ? lighter : darker;
|
||||||
|
darkColor = isOutset ? darker : lighter;
|
||||||
|
}
|
||||||
|
|
||||||
|
context.fillStyle = lightColor;
|
||||||
|
context.fillRect(0, 0, w - t, t);
|
||||||
|
context.fillRect(0, t, t, h - t);
|
||||||
|
|
||||||
|
context.fillStyle = darkColor;
|
||||||
|
context.fillRect(t, h - t, w - t, t);
|
||||||
|
context.fillRect(w - t, 0, t, h - t);
|
||||||
|
|
||||||
|
this.drawBevelCorners(context, t, w, h, lightColor, darkColor, isOutset);
|
||||||
|
}
|
||||||
|
|
||||||
|
drawBevelCorners(ctx, t, w, h, light, dark, isOutset) {
|
||||||
|
// Top-left corner
|
||||||
|
ctx.fillStyle = dark;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(0, h);
|
||||||
|
ctx.lineTo(t, h);
|
||||||
|
ctx.lineTo(t, h - t);
|
||||||
|
ctx.closePath();
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
// Bottom-right corner
|
||||||
|
ctx.fillStyle = light;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(w - t, 0);
|
||||||
|
ctx.lineTo(w - t, t);
|
||||||
|
ctx.lineTo(w, 0);
|
||||||
|
ctx.closePath();
|
||||||
|
ctx.fill();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -113,24 +187,212 @@ export class BorderEffect extends ButtonEffect {
|
||||||
*/
|
*/
|
||||||
drawRidgeBorder(context, width, renderData) {
|
drawRidgeBorder(context, width, renderData) {
|
||||||
// Outer ridge (light)
|
// Outer ridge (light)
|
||||||
context.strokeStyle = '#ffffff';
|
context.strokeStyle = "#ffffff";
|
||||||
context.lineWidth = width / 2;
|
context.lineWidth = width / 2;
|
||||||
context.strokeRect(
|
context.strokeRect(
|
||||||
width / 4,
|
width / 4,
|
||||||
width / 4,
|
width / 4,
|
||||||
renderData.width - width / 2,
|
renderData.width - width / 2,
|
||||||
renderData.height - width / 2
|
renderData.height - width / 2,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Inner ridge (dark)
|
// Inner ridge (dark)
|
||||||
context.strokeStyle = '#000000';
|
context.strokeStyle = "#000000";
|
||||||
context.strokeRect(
|
context.strokeRect(
|
||||||
(width * 3) / 4,
|
(width * 3) / 4,
|
||||||
(width * 3) / 4,
|
(width * 3) / 4,
|
||||||
renderData.width - width * 1.5,
|
renderData.width - width * 1.5,
|
||||||
renderData.height - width * 1.5
|
renderData.height - width * 1.5,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
adjustColor(hex, amount) {
|
||||||
|
// hex: "#rrggbb", amount: -1.0 .. 1.0
|
||||||
|
let r = parseInt(hex.slice(1, 3), 16);
|
||||||
|
let g = parseInt(hex.slice(3, 5), 16);
|
||||||
|
let b = parseInt(hex.slice(5, 7), 16);
|
||||||
|
|
||||||
|
const adjust = (c) =>
|
||||||
|
Math.min(255, Math.max(0, Math.round(c + amount * 255)));
|
||||||
|
|
||||||
|
r = adjust(r);
|
||||||
|
g = adjust(g);
|
||||||
|
b = adjust(b);
|
||||||
|
|
||||||
|
return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw dashed border
|
||||||
|
*/
|
||||||
|
drawDashedBorder(context, width, color, renderData) {
|
||||||
|
context.strokeStyle = color;
|
||||||
|
context.lineWidth = width;
|
||||||
|
context.setLineDash([6, 3]); // 6px dash, 3px gap
|
||||||
|
context.strokeRect(
|
||||||
|
width / 2,
|
||||||
|
width / 2,
|
||||||
|
renderData.width - width,
|
||||||
|
renderData.height - width
|
||||||
|
);
|
||||||
|
context.setLineDash([]); // Reset to solid
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw dotted border
|
||||||
|
*/
|
||||||
|
drawDottedBorder(context, width, color, renderData) {
|
||||||
|
context.strokeStyle = color;
|
||||||
|
context.lineWidth = width;
|
||||||
|
context.setLineDash([2, 3]); // 2px dot, 3px gap
|
||||||
|
context.lineCap = "round";
|
||||||
|
context.strokeRect(
|
||||||
|
width / 2,
|
||||||
|
width / 2,
|
||||||
|
renderData.width - width,
|
||||||
|
renderData.height - width
|
||||||
|
);
|
||||||
|
context.setLineDash([]); // Reset to solid
|
||||||
|
context.lineCap = "butt";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw double border
|
||||||
|
*/
|
||||||
|
drawDoubleBorder(context, width, color, renderData) {
|
||||||
|
const gap = Math.max(1, Math.floor(width / 3));
|
||||||
|
const lineWidth = Math.max(1, Math.floor((width - gap) / 2));
|
||||||
|
|
||||||
|
context.strokeStyle = color;
|
||||||
|
context.lineWidth = lineWidth;
|
||||||
|
|
||||||
|
// Outer border
|
||||||
|
context.strokeRect(
|
||||||
|
lineWidth / 2,
|
||||||
|
lineWidth / 2,
|
||||||
|
renderData.width - lineWidth,
|
||||||
|
renderData.height - lineWidth
|
||||||
|
);
|
||||||
|
|
||||||
|
// Inner border
|
||||||
|
const innerOffset = lineWidth + gap;
|
||||||
|
context.strokeRect(
|
||||||
|
innerOffset + lineWidth / 2,
|
||||||
|
innerOffset + lineWidth / 2,
|
||||||
|
renderData.width - innerOffset * 2 - lineWidth,
|
||||||
|
renderData.height - innerOffset * 2 - lineWidth
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw rainbow animated border
|
||||||
|
*/
|
||||||
|
drawRainbowBorder(context, width, animState, speed, renderData) {
|
||||||
|
if (!animState) {
|
||||||
|
// Fallback to solid if no animation
|
||||||
|
this.drawSolidBorder(context, width, "#ff0000", renderData);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hue = (animState.progress * speed * 360) % 360;
|
||||||
|
const color = `hsl(${hue}, 80%, 50%)`;
|
||||||
|
|
||||||
|
context.strokeStyle = color;
|
||||||
|
context.lineWidth = width;
|
||||||
|
context.strokeRect(
|
||||||
|
width / 2,
|
||||||
|
width / 2,
|
||||||
|
renderData.width - width,
|
||||||
|
renderData.height - width
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw marching ants animated border
|
||||||
|
*/
|
||||||
|
drawMarchingAntsBorder(context, width, animState, speed, renderData) {
|
||||||
|
if (!animState) {
|
||||||
|
// Fallback to dashed if no animation
|
||||||
|
this.drawDashedBorder(context, width, "#000000", renderData);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Animate the dash offset using phase for smooth looping
|
||||||
|
const phase = animState.getPhase(speed);
|
||||||
|
const dashLength = 9; // 4px dash + 5px gap = 9px total
|
||||||
|
const offset = (phase / (Math.PI * 2)) * dashLength;
|
||||||
|
|
||||||
|
context.strokeStyle = "#000000";
|
||||||
|
context.lineWidth = width;
|
||||||
|
context.setLineDash([4, 5]);
|
||||||
|
context.lineDashOffset = -offset;
|
||||||
|
context.strokeRect(
|
||||||
|
width / 2,
|
||||||
|
width / 2,
|
||||||
|
renderData.width - width,
|
||||||
|
renderData.height - width
|
||||||
|
);
|
||||||
|
context.setLineDash([]);
|
||||||
|
context.lineDashOffset = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw checkerboard border
|
||||||
|
*/
|
||||||
|
drawCheckerboardBorder(context, width, color, renderData) {
|
||||||
|
const squareSize = Math.max(2, width);
|
||||||
|
const w = renderData.width;
|
||||||
|
const h = renderData.height;
|
||||||
|
|
||||||
|
// Parse the color
|
||||||
|
const r = parseInt(color.slice(1, 3), 16);
|
||||||
|
const g = parseInt(color.slice(3, 5), 16);
|
||||||
|
const b = parseInt(color.slice(5, 7), 16);
|
||||||
|
|
||||||
|
// Create light and dark versions
|
||||||
|
const darkColor = color;
|
||||||
|
const lightColor = `rgb(${Math.min(255, r + 60)}, ${Math.min(
|
||||||
|
255,
|
||||||
|
g + 60
|
||||||
|
)}, ${Math.min(255, b + 60)})`;
|
||||||
|
|
||||||
|
// Draw checkerboard on all four sides
|
||||||
|
// Top
|
||||||
|
for (let x = 0; x < w; x += squareSize) {
|
||||||
|
for (let y = 0; y < width; y += squareSize) {
|
||||||
|
const checker = Math.floor(x / squareSize + y / squareSize) % 2;
|
||||||
|
context.fillStyle = checker === 0 ? darkColor : lightColor;
|
||||||
|
context.fillRect(x, y, squareSize, squareSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bottom
|
||||||
|
for (let x = 0; x < w; x += squareSize) {
|
||||||
|
for (let y = h - width; y < h; y += squareSize) {
|
||||||
|
const checker = Math.floor(x / squareSize + y / squareSize) % 2;
|
||||||
|
context.fillStyle = checker === 0 ? darkColor : lightColor;
|
||||||
|
context.fillRect(x, y, squareSize, squareSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Left
|
||||||
|
for (let x = 0; x < width; x += squareSize) {
|
||||||
|
for (let y = width; y < h - width; y += squareSize) {
|
||||||
|
const checker = Math.floor(x / squareSize + y / squareSize) % 2;
|
||||||
|
context.fillStyle = checker === 0 ? darkColor : lightColor;
|
||||||
|
context.fillRect(x, y, squareSize, squareSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Right
|
||||||
|
for (let x = w - width; x < w; x += squareSize) {
|
||||||
|
for (let y = width; y < h - width; y += squareSize) {
|
||||||
|
const checker = Math.floor(x / squareSize + y / squareSize) % 2;
|
||||||
|
context.fillStyle = checker === 0 ? darkColor : lightColor;
|
||||||
|
context.fillRect(x, y, squareSize, squareSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-register effect
|
// Auto-register effect
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { ButtonEffect } from '../effect-base.js';
|
import { ButtonEffect } from "../effect-base.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Spinning text animation effect
|
* Spinning text animation effect
|
||||||
|
|
@ -6,61 +6,62 @@ import { ButtonEffect } from '../effect-base.js';
|
||||||
*/
|
*/
|
||||||
export class SpinTextEffect extends ButtonEffect {
|
export class SpinTextEffect extends ButtonEffect {
|
||||||
constructor(textLineNumber = 1) {
|
constructor(textLineNumber = 1) {
|
||||||
const suffix = textLineNumber === 1 ? '' : '2';
|
const suffix = textLineNumber === 1 ? "" : "2";
|
||||||
super({
|
super({
|
||||||
id: `text-spin${suffix}`,
|
id: `text-spin${suffix}`,
|
||||||
name: `Spinning Text ${textLineNumber}`,
|
name: `Spinning Text ${textLineNumber}`,
|
||||||
type: textLineNumber === 1 ? 'text' : 'text2',
|
type: textLineNumber === 1 ? "text" : "text2",
|
||||||
category: textLineNumber === 1 ? 'Text Line 1' : 'Text Line 2',
|
category: textLineNumber === 1 ? "Text Line 1" : "Text Line 2",
|
||||||
renderOrder: 8, // Before wave, after rainbow
|
renderOrder: 8, // Before wave, after rainbow
|
||||||
textLineNumber: textLineNumber
|
textLineNumber: textLineNumber,
|
||||||
});
|
});
|
||||||
this.textLineNumber = textLineNumber;
|
this.textLineNumber = textLineNumber;
|
||||||
}
|
}
|
||||||
|
|
||||||
defineControls() {
|
defineControls() {
|
||||||
const textLineNumber = this.textLineNumber || this.config?.textLineNumber || 1;
|
const textLineNumber =
|
||||||
const suffix = textLineNumber === 1 ? '' : '2';
|
this.textLineNumber || this.config?.textLineNumber || 1;
|
||||||
|
const suffix = textLineNumber === 1 ? "" : "2";
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
id: `animate-text-spin${suffix}`,
|
id: `animate-text-spin${suffix}`,
|
||||||
type: 'checkbox',
|
type: "checkbox",
|
||||||
label: 'Spinning Animation',
|
label: "Spinning Animation",
|
||||||
defaultValue: false
|
defaultValue: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: `spin-speed${suffix}`,
|
id: `spin-speed${suffix}`,
|
||||||
type: 'range',
|
type: "range",
|
||||||
label: 'Spin Speed',
|
label: "Spin Speed",
|
||||||
defaultValue: 1,
|
defaultValue: 1,
|
||||||
min: 0.1,
|
min: 1,
|
||||||
max: 5,
|
max: 5,
|
||||||
step: 0.1,
|
step: 1,
|
||||||
showWhen: `animate-text-spin${suffix}`,
|
showWhen: `animate-text-spin${suffix}`,
|
||||||
description: 'Speed of character rotation'
|
description: "Speed of character rotation",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: `spin-stagger${suffix}`,
|
id: `spin-stagger${suffix}`,
|
||||||
type: 'range',
|
type: "range",
|
||||||
label: 'Spin Stagger',
|
label: "Spin Stagger",
|
||||||
defaultValue: 0.3,
|
defaultValue: 0.3,
|
||||||
min: 0,
|
min: 0,
|
||||||
max: 1,
|
max: 1,
|
||||||
step: 0.1,
|
step: 0.1,
|
||||||
showWhen: `animate-text-spin${suffix}`,
|
showWhen: `animate-text-spin${suffix}`,
|
||||||
description: 'Delay between characters'
|
description: "Delay between characters",
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
isEnabled(controlValues) {
|
isEnabled(controlValues) {
|
||||||
const suffix = this.textLineNumber === 1 ? '' : '2';
|
const suffix = this.textLineNumber === 1 ? "" : "2";
|
||||||
return controlValues[`animate-text-spin${suffix}`] === true;
|
return controlValues[`animate-text-spin${suffix}`] === true;
|
||||||
}
|
}
|
||||||
|
|
||||||
apply(context, controlValues, animState, renderData) {
|
apply(context, controlValues, animState, renderData) {
|
||||||
const suffix = this.textLineNumber === 1 ? '' : '2';
|
const suffix = this.textLineNumber === 1 ? "" : "2";
|
||||||
const text = controlValues[`button-text${suffix}`] || '';
|
const text = controlValues[`button-text${suffix}`] || "";
|
||||||
|
|
||||||
if (!text || !controlValues[`text${suffix}-enabled`]) return;
|
if (!text || !controlValues[`text${suffix}-enabled`]) return;
|
||||||
if (!animState) return;
|
if (!animState) return;
|
||||||
|
|
@ -68,21 +69,26 @@ export class SpinTextEffect extends ButtonEffect {
|
||||||
const speed = controlValues[`spin-speed${suffix}`] || 1;
|
const speed = controlValues[`spin-speed${suffix}`] || 1;
|
||||||
const stagger = controlValues[`spin-stagger${suffix}`] || 0.3;
|
const stagger = controlValues[`spin-stagger${suffix}`] || 0.3;
|
||||||
const fontSize = controlValues[`font-size${suffix}`] || 14;
|
const fontSize = controlValues[`font-size${suffix}`] || 14;
|
||||||
const fontFamily = controlValues[`font-family${suffix}`] || 'Arial';
|
const fontFamily = controlValues[`font-family${suffix}`] || "Arial";
|
||||||
const fontWeight = controlValues[`text${suffix}-bold`] ? 'bold' : 'normal';
|
const fontWeight = controlValues[`text${suffix}-bold`] ? "bold" : "normal";
|
||||||
const fontStyle = controlValues[`text${suffix}-italic`] ? 'italic' : 'normal';
|
const fontStyle = controlValues[`text${suffix}-italic`]
|
||||||
|
? "italic"
|
||||||
|
: "normal";
|
||||||
|
|
||||||
context.font = `${fontStyle} ${fontWeight} ${fontSize}px "${fontFamily}"`;
|
context.font = `${fontStyle} ${fontWeight} ${fontSize}px "${fontFamily}"`;
|
||||||
context.textAlign = 'center';
|
context.textAlign = "center";
|
||||||
context.textBaseline = 'middle';
|
context.textBaseline = "middle";
|
||||||
|
|
||||||
// Get text color
|
// Get text color
|
||||||
let fillStyle;
|
let fillStyle;
|
||||||
const colorType = controlValues[`text${suffix}-color-type`] || 'solid';
|
const colorType = controlValues[`text${suffix}-color-type`] || "solid";
|
||||||
if (colorType === 'gradient') {
|
if (colorType === "gradient") {
|
||||||
const color1 = controlValues[`text${suffix}-gradient-color1`] || '#ffffff';
|
const color1 =
|
||||||
const color2 = controlValues[`text${suffix}-gradient-color2`] || '#00ffff';
|
controlValues[`text${suffix}-gradient-color1`] || "#ffffff";
|
||||||
const angle = (controlValues[`text${suffix}-gradient-angle`] || 90) * (Math.PI / 180);
|
const color2 =
|
||||||
|
controlValues[`text${suffix}-gradient-color2`] || "#00ffff";
|
||||||
|
const angle =
|
||||||
|
(controlValues[`text${suffix}-gradient-angle`] || 90) * (Math.PI / 180);
|
||||||
const centerX = renderData.centerX;
|
const centerX = renderData.centerX;
|
||||||
const centerY = renderData.centerY;
|
const centerY = renderData.centerY;
|
||||||
const x1 = centerX + Math.cos(angle) * 20;
|
const x1 = centerX + Math.cos(angle) * 20;
|
||||||
|
|
@ -94,7 +100,7 @@ export class SpinTextEffect extends ButtonEffect {
|
||||||
gradient.addColorStop(1, color2);
|
gradient.addColorStop(1, color2);
|
||||||
fillStyle = gradient;
|
fillStyle = gradient;
|
||||||
} else {
|
} else {
|
||||||
fillStyle = controlValues[`text${suffix}-color`] || '#ffffff';
|
fillStyle = controlValues[`text${suffix}-color`] || "#ffffff";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate base position
|
// Calculate base position
|
||||||
|
|
@ -103,13 +109,24 @@ export class SpinTextEffect extends ButtonEffect {
|
||||||
const baseX = (x / 100) * renderData.width;
|
const baseX = (x / 100) * renderData.width;
|
||||||
const baseY = (y / 100) * renderData.height;
|
const baseY = (y / 100) * renderData.height;
|
||||||
|
|
||||||
|
// Split text into grapheme clusters (handles emojis properly)
|
||||||
|
// Use Intl.Segmenter if available, otherwise fall back to spread operator
|
||||||
|
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 {
|
||||||
|
// Fallback: spread operator handles basic emoji
|
||||||
|
chars = [...text];
|
||||||
|
}
|
||||||
|
|
||||||
// Measure total text width for centering
|
// Measure total text width for centering
|
||||||
const totalWidth = context.measureText(text).width;
|
const totalWidth = context.measureText(text).width;
|
||||||
let currentX = baseX - totalWidth / 2;
|
let currentX = baseX - totalWidth / 2;
|
||||||
|
|
||||||
// Draw each character with rotation
|
// Draw each character with rotation
|
||||||
for (let i = 0; i < text.length; i++) {
|
for (let i = 0; i < chars.length; i++) {
|
||||||
const char = text[i];
|
const char = chars[i];
|
||||||
const charWidth = context.measureText(char).width;
|
const charWidth = context.measureText(char).width;
|
||||||
const charCenterX = currentX + charWidth / 2;
|
const charCenterX = currentX + charWidth / 2;
|
||||||
|
|
||||||
|
|
@ -123,7 +140,8 @@ export class SpinTextEffect extends ButtonEffect {
|
||||||
|
|
||||||
// Apply outline if enabled
|
// Apply outline if enabled
|
||||||
if (controlValues[`text${suffix}-outline`]) {
|
if (controlValues[`text${suffix}-outline`]) {
|
||||||
context.strokeStyle = controlValues[`text${suffix}-outline-color`] || '#000000';
|
context.strokeStyle =
|
||||||
|
controlValues[`text${suffix}-outline-color`] || "#000000";
|
||||||
context.lineWidth = 2;
|
context.lineWidth = 2;
|
||||||
context.strokeText(char, 0, 0);
|
context.strokeText(char, 0, 0);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -70,10 +70,10 @@ export class SpotlightEffect extends ButtonEffect {
|
||||||
id: "spotlight-darkness",
|
id: "spotlight-darkness",
|
||||||
type: "range",
|
type: "range",
|
||||||
label: "Darkness",
|
label: "Darkness",
|
||||||
defaultValue: 0.5,
|
defaultValue: 1,
|
||||||
min: 0,
|
min: 0,
|
||||||
max: 1,
|
max: 1,
|
||||||
step: 0.05,
|
step: 0.1,
|
||||||
showWhen: "animate-spotlight",
|
showWhen: "animate-spotlight",
|
||||||
description: "How dark the non-spotlight area should be",
|
description: "How dark the non-spotlight area should be",
|
||||||
},
|
},
|
||||||
|
|
@ -84,9 +84,9 @@ export class SpotlightEffect extends ButtonEffect {
|
||||||
type: "range",
|
type: "range",
|
||||||
label: "Movement Speed",
|
label: "Movement Speed",
|
||||||
defaultValue: 1,
|
defaultValue: 1,
|
||||||
min: 0.1,
|
min: 1,
|
||||||
max: 3,
|
max: 3,
|
||||||
step: 0.1,
|
step: 1,
|
||||||
showWhen: "animate-spotlight",
|
showWhen: "animate-spotlight",
|
||||||
description: "Speed of spotlight movement",
|
description: "Speed of spotlight movement",
|
||||||
},
|
},
|
||||||
|
|
@ -152,16 +152,6 @@ export class SpotlightEffect extends ButtonEffect {
|
||||||
context.fill();
|
context.fill();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Optional: Override canApply for more complex logic
|
|
||||||
* By default, it just checks isEnabled()
|
|
||||||
*/
|
|
||||||
canApply(controlValues) {
|
|
||||||
// Example: Only apply if text is also enabled
|
|
||||||
const textEnabled = controlValues["textEnabled"];
|
|
||||||
return this.isEnabled(controlValues) && textEnabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Optional: Add helper methods for your effect
|
* Optional: Add helper methods for your effect
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
167
static/js/button-generator/effects/text-shadow.js
Normal file
167
static/js/button-generator/effects/text-shadow.js
Normal file
|
|
@ -0,0 +1,167 @@
|
||||||
|
import { ButtonEffect } from "../effect-base.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Text Drop Shadow Effect
|
||||||
|
* Renders text with a drop shadow underneath
|
||||||
|
* This draws the shadow first, then standard text rendering draws on top
|
||||||
|
*/
|
||||||
|
export class TextShadowEffect extends ButtonEffect {
|
||||||
|
constructor(textLineNumber = 1) {
|
||||||
|
const suffix = textLineNumber === 1 ? "" : "2";
|
||||||
|
super({
|
||||||
|
id: `text-shadow${suffix}`,
|
||||||
|
name: `Drop Shadow ${textLineNumber}`,
|
||||||
|
type: textLineNumber === 1 ? "text" : "text2",
|
||||||
|
category: textLineNumber === 1 ? "Text Line 1" : "Text Line 2",
|
||||||
|
renderOrder: 19, // Before standard text (20), so shadow draws first
|
||||||
|
textLineNumber: textLineNumber,
|
||||||
|
});
|
||||||
|
this.textLineNumber = textLineNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
defineControls() {
|
||||||
|
const textLineNumber =
|
||||||
|
this.textLineNumber || this.config?.textLineNumber || 1;
|
||||||
|
const suffix = textLineNumber === 1 ? "" : "2";
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: `text${suffix}-shadow-enabled`,
|
||||||
|
type: "checkbox",
|
||||||
|
label: "Drop Shadow",
|
||||||
|
defaultValue: false,
|
||||||
|
description:
|
||||||
|
"Add drop shadow to text - Not compatible with other text effects!!",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: `text${suffix}-shadow-color`,
|
||||||
|
type: "color",
|
||||||
|
label: "Shadow Color",
|
||||||
|
defaultValue: "#000000",
|
||||||
|
showWhen: `text${suffix}-shadow-enabled`,
|
||||||
|
description: "Color of the shadow",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: `text${suffix}-shadow-blur`,
|
||||||
|
type: "range",
|
||||||
|
label: "Shadow Blur",
|
||||||
|
defaultValue: 4,
|
||||||
|
min: 0,
|
||||||
|
max: 10,
|
||||||
|
step: 1,
|
||||||
|
showWhen: `text${suffix}-shadow-enabled`,
|
||||||
|
description: "Blur radius of shadow",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: `text${suffix}-shadow-offset-x`,
|
||||||
|
type: "range",
|
||||||
|
label: "Shadow X Offset",
|
||||||
|
defaultValue: 2,
|
||||||
|
min: -10,
|
||||||
|
max: 10,
|
||||||
|
step: 1,
|
||||||
|
showWhen: `text${suffix}-shadow-enabled`,
|
||||||
|
description: "Horizontal shadow offset",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: `text${suffix}-shadow-offset-y`,
|
||||||
|
type: "range",
|
||||||
|
label: "Shadow Y Offset",
|
||||||
|
defaultValue: 2,
|
||||||
|
min: -10,
|
||||||
|
max: 10,
|
||||||
|
step: 1,
|
||||||
|
showWhen: `text${suffix}-shadow-enabled`,
|
||||||
|
description: "Vertical shadow offset",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: `text${suffix}-shadow-opacity`,
|
||||||
|
type: "range",
|
||||||
|
label: "Shadow Opacity",
|
||||||
|
defaultValue: 0.8,
|
||||||
|
min: 0,
|
||||||
|
max: 1,
|
||||||
|
step: 0.1,
|
||||||
|
showWhen: `text${suffix}-shadow-enabled`,
|
||||||
|
description: "Opacity of the shadow",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
isEnabled(controlValues) {
|
||||||
|
const suffix = this.textLineNumber === 1 ? "" : "2";
|
||||||
|
const textEnabled = controlValues[`text${suffix}-enabled`];
|
||||||
|
const shadowEnabled = controlValues[`text${suffix}-shadow-enabled`];
|
||||||
|
return textEnabled && shadowEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
apply(context, controlValues, animState, renderData) {
|
||||||
|
const suffix = this.textLineNumber === 1 ? "" : "2";
|
||||||
|
|
||||||
|
const text = controlValues[`button-text${suffix}`] || "";
|
||||||
|
if (!text) return;
|
||||||
|
|
||||||
|
// Get shadow settings
|
||||||
|
const shadowColor =
|
||||||
|
controlValues[`text${suffix}-shadow-color`] || "#000000";
|
||||||
|
const shadowBlur = controlValues[`text${suffix}-shadow-blur`] || 4;
|
||||||
|
const shadowOffsetX = controlValues[`text${suffix}-shadow-offset-x`] || 2;
|
||||||
|
const shadowOffsetY = controlValues[`text${suffix}-shadow-offset-y`] || 2;
|
||||||
|
const shadowOpacity = controlValues[`text${suffix}-shadow-opacity`] || 0.8;
|
||||||
|
|
||||||
|
// Get text rendering 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";
|
||||||
|
const textX = (controlValues[`text${suffix}-x`] || 50) / 100;
|
||||||
|
const textY = (controlValues[`text${suffix}-y`] || 50) / 100;
|
||||||
|
|
||||||
|
// Convert hex to rgba
|
||||||
|
const hexToRgba = (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(0, 0, 0, ${alpha})`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Set up text rendering
|
||||||
|
context.font = `${fontStyle} ${fontWeight} ${fontSize}px "${fontFamily}"`;
|
||||||
|
context.textAlign = "center";
|
||||||
|
context.textBaseline = "middle";
|
||||||
|
|
||||||
|
// Calculate text position
|
||||||
|
const x = renderData.width * textX;
|
||||||
|
const y = renderData.height * textY;
|
||||||
|
|
||||||
|
// Draw the shadow using the shadow API
|
||||||
|
// This will create a shadow underneath whatever we draw
|
||||||
|
context.shadowColor = hexToRgba(shadowColor, shadowOpacity);
|
||||||
|
context.shadowBlur = shadowBlur;
|
||||||
|
context.shadowOffsetX = shadowOffsetX;
|
||||||
|
context.shadowOffsetY = shadowOffsetY;
|
||||||
|
|
||||||
|
// Draw a solid shadow by filling with the shadow color
|
||||||
|
// The shadow API will create the blur effect
|
||||||
|
context.fillStyle = hexToRgba(shadowColor, shadowOpacity);
|
||||||
|
context.fillText(text, x, y);
|
||||||
|
|
||||||
|
// Reset shadow for subsequent renders
|
||||||
|
context.shadowColor = "transparent";
|
||||||
|
context.shadowBlur = 0;
|
||||||
|
context.shadowOffsetX = 0;
|
||||||
|
context.shadowOffsetY = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export two instances for text line 1 and text line 2
|
||||||
|
export function register(generator) {
|
||||||
|
generator.registerEffect(new TextShadowEffect(1));
|
||||||
|
generator.registerEffect(new TextShadowEffect(2));
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { ButtonEffect } from '../effect-base.js';
|
import { ButtonEffect } from "../effect-base.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Standard text rendering effect
|
* Standard text rendering effect
|
||||||
|
|
@ -6,180 +6,184 @@ import { ButtonEffect } from '../effect-base.js';
|
||||||
*/
|
*/
|
||||||
export class StandardTextEffect extends ButtonEffect {
|
export class StandardTextEffect extends ButtonEffect {
|
||||||
constructor(textLineNumber = 1) {
|
constructor(textLineNumber = 1) {
|
||||||
const suffix = textLineNumber === 1 ? '' : '2';
|
const suffix = textLineNumber === 1 ? "" : "2";
|
||||||
super({
|
super({
|
||||||
id: `text-standard${suffix}`,
|
id: `text-standard${suffix}`,
|
||||||
name: `Standard Text ${textLineNumber}`,
|
name: `Standard Text ${textLineNumber}`,
|
||||||
type: textLineNumber === 1 ? 'text' : 'text2',
|
type: textLineNumber === 1 ? "text" : "text2",
|
||||||
category: textLineNumber === 1 ? 'Text Line 1' : 'Text Line 2',
|
category: textLineNumber === 1 ? "Text Line 1" : "Text Line 2",
|
||||||
renderOrder: 20, // After animations
|
renderOrder: 20, // After animations
|
||||||
textLineNumber: textLineNumber // Pass through config so defineControls can access it
|
textLineNumber: textLineNumber, // Pass through config so defineControls can access it
|
||||||
});
|
});
|
||||||
this.textLineNumber = textLineNumber;
|
this.textLineNumber = textLineNumber;
|
||||||
}
|
}
|
||||||
|
|
||||||
defineControls() {
|
defineControls() {
|
||||||
// Access from config since this is called before constructor completes
|
// Access from config since this is called before constructor completes
|
||||||
const textLineNumber = this.textLineNumber || this.config?.textLineNumber || 1;
|
const textLineNumber =
|
||||||
const suffix = textLineNumber === 1 ? '' : '2';
|
this.textLineNumber || this.config?.textLineNumber || 1;
|
||||||
|
const suffix = textLineNumber === 1 ? "" : "2";
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
id: `button-text${suffix}`,
|
id: `button-text${suffix}`,
|
||||||
type: 'text',
|
type: "text",
|
||||||
label: `Text Line ${textLineNumber}`,
|
label: `Text Line ${textLineNumber}`,
|
||||||
defaultValue: textLineNumber === 1 ? 'RITUAL.SH' : ''
|
defaultValue: textLineNumber === 1 ? "RITUAL.SH" : "",
|
||||||
},
|
|
||||||
{
|
|
||||||
id: `text${suffix}-enabled`,
|
|
||||||
type: 'checkbox',
|
|
||||||
label: `Enable Text Line ${textLineNumber}`,
|
|
||||||
defaultValue: textLineNumber === 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: `font-size${suffix}`,
|
id: `font-size${suffix}`,
|
||||||
type: 'range',
|
type: "range",
|
||||||
label: 'Font Size',
|
label: "Font Size",
|
||||||
min: 6,
|
min: 6,
|
||||||
max: 24,
|
max: 24,
|
||||||
defaultValue: textLineNumber === 1 ? 14 : 12
|
defaultValue: textLineNumber === 1 ? 14 : 12,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: `text${suffix}-x`,
|
id: `text${suffix}-x`,
|
||||||
type: 'range',
|
type: "range",
|
||||||
label: 'Horizontal Position',
|
label: "Horizontal Position",
|
||||||
min: 0,
|
min: 0,
|
||||||
max: 100,
|
max: 100,
|
||||||
defaultValue: 50,
|
defaultValue: 50,
|
||||||
description: 'Percentage from left'
|
description: "Percentage from left",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: `text${suffix}-y`,
|
id: `text${suffix}-y`,
|
||||||
type: 'range',
|
type: "range",
|
||||||
label: 'Vertical Position',
|
label: "Vertical Position",
|
||||||
min: 0,
|
min: 0,
|
||||||
max: 100,
|
max: 100,
|
||||||
defaultValue: textLineNumber === 1 ? 35 : 65,
|
defaultValue: textLineNumber === 1 ? 35 : 65,
|
||||||
description: 'Percentage from top'
|
description: "Percentage from top",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: `text${suffix}-color-type`,
|
id: `text${suffix}-color-type`,
|
||||||
type: 'select',
|
type: "select",
|
||||||
label: 'Color Type',
|
label: "Color Type",
|
||||||
defaultValue: 'solid',
|
defaultValue: "solid",
|
||||||
options: [
|
options: [
|
||||||
{ value: 'solid', label: 'Solid Color' },
|
{ value: "solid", label: "Solid Color" },
|
||||||
{ value: 'gradient', label: 'Gradient' }
|
{ value: "gradient", label: "Gradient" },
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: `text${suffix}-color`,
|
id: `text${suffix}-color`,
|
||||||
type: 'color',
|
type: "color",
|
||||||
label: 'Text Color',
|
label: "Text Color",
|
||||||
defaultValue: '#ffffff',
|
defaultValue: "#ffffff",
|
||||||
showWhen: `text${suffix}-color-type`
|
showWhen: `text${suffix}-color-type`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: `text${suffix}-gradient-color1`,
|
id: `text${suffix}-gradient-color1`,
|
||||||
type: 'color',
|
type: "color",
|
||||||
label: 'Gradient Color 1',
|
label: "Gradient Color 1",
|
||||||
defaultValue: '#ffffff',
|
defaultValue: "#ffffff",
|
||||||
showWhen: `text${suffix}-color-type`
|
showWhen: `text${suffix}-color-type`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: `text${suffix}-gradient-color2`,
|
id: `text${suffix}-gradient-color2`,
|
||||||
type: 'color',
|
type: "color",
|
||||||
label: 'Gradient Color 2',
|
label: "Gradient Color 2",
|
||||||
defaultValue: '#00ffff',
|
defaultValue: "#00ffff",
|
||||||
showWhen: `text${suffix}-color-type`
|
showWhen: `text${suffix}-color-type`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: `text${suffix}-gradient-angle`,
|
id: `text${suffix}-gradient-angle`,
|
||||||
type: 'range',
|
type: "range",
|
||||||
label: 'Gradient Angle',
|
label: "Gradient Angle",
|
||||||
min: 0,
|
min: 0,
|
||||||
max: 360,
|
max: 360,
|
||||||
defaultValue: 0,
|
defaultValue: 0,
|
||||||
showWhen: `text${suffix}-color-type`
|
showWhen: `text${suffix}-color-type`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: `text${suffix}-outline`,
|
id: `text${suffix}-outline`,
|
||||||
type: 'checkbox',
|
type: "checkbox",
|
||||||
label: 'Outline',
|
label: "Outline",
|
||||||
defaultValue: false
|
defaultValue: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: `outline${suffix}-color`,
|
id: `outline${suffix}-color`,
|
||||||
type: 'color',
|
type: "color",
|
||||||
label: 'Outline Color',
|
label: "Outline Color",
|
||||||
defaultValue: '#000000',
|
defaultValue: "#000000",
|
||||||
showWhen: `text${suffix}-outline`
|
showWhen: `text${suffix}-outline`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: `font-family${suffix}`,
|
id: `font-family${suffix}`,
|
||||||
type: 'select',
|
type: "select",
|
||||||
label: 'Font',
|
label: "Font",
|
||||||
defaultValue: 'Lato',
|
defaultValue: "Lato",
|
||||||
options: [
|
options: [
|
||||||
{ value: 'Lato', label: 'Lato' },
|
{ value: "Lato", label: "Lato" },
|
||||||
{ value: 'Roboto', label: 'Roboto' },
|
{ value: "Roboto", label: "Roboto" },
|
||||||
{ value: 'Open Sans', label: 'Open Sans' },
|
{ value: "Open Sans", label: "Open Sans" },
|
||||||
{ value: 'Montserrat', label: 'Montserrat' },
|
{ value: "Montserrat", label: "Montserrat" },
|
||||||
{ value: 'Oswald', label: 'Oswald' },
|
{ value: "Oswald", label: "Oswald" },
|
||||||
{ value: 'Bebas Neue', label: 'Bebas Neue' },
|
{ value: "Bebas Neue", label: "Bebas Neue" },
|
||||||
{ value: 'Roboto Mono', label: 'Roboto Mono' },
|
{ value: "Roboto Mono", label: "Roboto Mono" },
|
||||||
{ value: 'VT323', label: 'VT323' },
|
{ value: "VT323", label: "VT323" },
|
||||||
{ value: 'Press Start 2P', label: 'Press Start 2P' },
|
{ value: "Press Start 2P", label: "Press Start 2P" },
|
||||||
{ value: 'DSEG7-Classic', label: 'DSEG7' }
|
{ value: "DSEG7-Classic", label: "DSEG7" },
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: `font-bold${suffix}`,
|
id: `font-bold${suffix}`,
|
||||||
type: 'checkbox',
|
type: "checkbox",
|
||||||
label: 'Bold',
|
label: "Bold",
|
||||||
defaultValue: false
|
defaultValue: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: `font-italic${suffix}`,
|
id: `font-italic${suffix}`,
|
||||||
type: 'checkbox',
|
type: "checkbox",
|
||||||
label: 'Italic',
|
label: "Italic",
|
||||||
defaultValue: false
|
defaultValue: false,
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
isEnabled(controlValues) {
|
isEnabled(controlValues) {
|
||||||
const suffix = this.textLineNumber === 1 ? '' : '2';
|
const suffix = this.textLineNumber === 1 ? "" : "2";
|
||||||
const text = controlValues[`button-text${suffix}`];
|
const text = controlValues[`button-text${suffix}`];
|
||||||
const enabled = controlValues[`text${suffix}-enabled`];
|
|
||||||
|
|
||||||
// Only render if text exists, is enabled, and no animations are active on this text
|
// Only render if text exists and no animations are active on this text
|
||||||
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}`];
|
||||||
|
|
||||||
return text && enabled && !waveActive && !rainbowActive;
|
return text && text.trim() !== "" && !waveActive && !rainbowActive && !spinActive;
|
||||||
}
|
}
|
||||||
|
|
||||||
apply(context, controlValues, animState, renderData) {
|
apply(context, controlValues, animState, renderData) {
|
||||||
const suffix = this.textLineNumber === 1 ? '' : '2';
|
const suffix = this.textLineNumber === 1 ? "" : "2";
|
||||||
|
|
||||||
const text = controlValues[`button-text${suffix}`];
|
const text = controlValues[`button-text${suffix}`];
|
||||||
if (!text) return;
|
if (!text) return;
|
||||||
|
|
||||||
const fontSize = controlValues[`font-size${suffix}`] || 12;
|
const fontSize = controlValues[`font-size${suffix}`] || 12;
|
||||||
const fontWeight = controlValues[`font-bold${suffix}`] ? 'bold' : 'normal';
|
const fontWeight = controlValues[`font-bold${suffix}`] ? "bold" : "normal";
|
||||||
const fontStyle = controlValues[`font-italic${suffix}`] ? 'italic' : 'normal';
|
const fontStyle = controlValues[`font-italic${suffix}`]
|
||||||
const fontFamily = controlValues[`font-family${suffix}`] || 'Arial';
|
? "italic"
|
||||||
|
: "normal";
|
||||||
|
const fontFamily = controlValues[`font-family${suffix}`] || "Arial";
|
||||||
|
|
||||||
const x = (controlValues[`text${suffix}-x`] / 100) * renderData.width;
|
const x = (controlValues[`text${suffix}-x`] / 100) * renderData.width;
|
||||||
const y = (controlValues[`text${suffix}-y`] / 100) * renderData.height;
|
const y = (controlValues[`text${suffix}-y`] / 100) * renderData.height;
|
||||||
|
|
||||||
// Set font
|
// Set font
|
||||||
context.font = `${fontStyle} ${fontWeight} ${fontSize}px "${fontFamily}"`;
|
context.font = `${fontStyle} ${fontWeight} ${fontSize}px "${fontFamily}"`;
|
||||||
context.textAlign = 'center';
|
context.textAlign = "center";
|
||||||
context.textBaseline = 'middle';
|
context.textBaseline = "middle";
|
||||||
|
|
||||||
// Get colors
|
// Get colors
|
||||||
const colors = this.getTextColors(context, controlValues, text, x, y, fontSize);
|
const colors = this.getTextColors(
|
||||||
|
context,
|
||||||
|
controlValues,
|
||||||
|
text,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
fontSize,
|
||||||
|
);
|
||||||
|
|
||||||
// Draw outline if enabled
|
// Draw outline if enabled
|
||||||
if (controlValues[`text${suffix}-outline`]) {
|
if (controlValues[`text${suffix}-outline`]) {
|
||||||
|
|
@ -197,17 +201,18 @@ export class StandardTextEffect extends ButtonEffect {
|
||||||
* Get text colors (solid or gradient)
|
* Get text colors (solid or gradient)
|
||||||
*/
|
*/
|
||||||
getTextColors(context, controlValues, text, x, y, fontSize) {
|
getTextColors(context, controlValues, text, x, y, fontSize) {
|
||||||
const suffix = this.textLineNumber === 1 ? '' : '2';
|
const suffix = this.textLineNumber === 1 ? "" : "2";
|
||||||
const colorType = controlValues[`text${suffix}-color-type`] || 'solid';
|
const colorType = controlValues[`text${suffix}-color-type`] || "solid";
|
||||||
|
|
||||||
let fillStyle, strokeStyle;
|
let fillStyle, strokeStyle;
|
||||||
|
|
||||||
if (colorType === 'solid') {
|
if (colorType === "solid") {
|
||||||
fillStyle = controlValues[`text${suffix}-color`] || '#ffffff';
|
fillStyle = controlValues[`text${suffix}-color`] || "#ffffff";
|
||||||
strokeStyle = controlValues[`outline${suffix}-color`] || '#000000';
|
strokeStyle = controlValues[`outline${suffix}-color`] || "#000000";
|
||||||
} else {
|
} else {
|
||||||
// Gradient
|
// Gradient
|
||||||
const angle = (controlValues[`text${suffix}-gradient-angle`] || 0) * (Math.PI / 180);
|
const angle =
|
||||||
|
(controlValues[`text${suffix}-gradient-angle`] || 0) * (Math.PI / 180);
|
||||||
const textWidth = context.measureText(text).width;
|
const textWidth = context.measureText(text).width;
|
||||||
const x1 = x - textWidth / 2 + (Math.cos(angle) * textWidth) / 2;
|
const x1 = x - textWidth / 2 + (Math.cos(angle) * textWidth) / 2;
|
||||||
const y1 = y - fontSize / 2 + (Math.sin(angle) * fontSize) / 2;
|
const y1 = y - fontSize / 2 + (Math.sin(angle) * fontSize) / 2;
|
||||||
|
|
@ -215,10 +220,16 @@ export class StandardTextEffect extends ButtonEffect {
|
||||||
const y2 = y + fontSize / 2 - (Math.sin(angle) * fontSize) / 2;
|
const y2 = y + fontSize / 2 - (Math.sin(angle) * fontSize) / 2;
|
||||||
|
|
||||||
const gradient = context.createLinearGradient(x1, y1, x2, y2);
|
const gradient = context.createLinearGradient(x1, y1, x2, y2);
|
||||||
gradient.addColorStop(0, controlValues[`text${suffix}-gradient-color1`] || '#ffffff');
|
gradient.addColorStop(
|
||||||
gradient.addColorStop(1, controlValues[`text${suffix}-gradient-color2`] || '#00ffff');
|
0,
|
||||||
|
controlValues[`text${suffix}-gradient-color1`] || "#ffffff",
|
||||||
|
);
|
||||||
|
gradient.addColorStop(
|
||||||
|
1,
|
||||||
|
controlValues[`text${suffix}-gradient-color2`] || "#00ffff",
|
||||||
|
);
|
||||||
fillStyle = gradient;
|
fillStyle = gradient;
|
||||||
strokeStyle = controlValues[`outline${suffix}-color`] || '#000000';
|
strokeStyle = controlValues[`outline${suffix}-color`] || "#000000";
|
||||||
}
|
}
|
||||||
|
|
||||||
return { fillStyle, strokeStyle };
|
return { fillStyle, strokeStyle };
|
||||||
|
|
|
||||||
|
|
@ -87,18 +87,29 @@ export class WaveTextEffect extends ButtonEffect {
|
||||||
// Get colors
|
// Get colors
|
||||||
const colors = this.getTextColors(context, controlValues, text, baseX, baseY, fontSize, animState);
|
const colors = this.getTextColors(context, controlValues, text, baseX, baseY, fontSize, animState);
|
||||||
|
|
||||||
|
// Split text into grapheme clusters (handles emojis properly)
|
||||||
|
// Use Intl.Segmenter if available, otherwise fall back to spread operator
|
||||||
|
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 {
|
||||||
|
// Fallback: spread operator handles basic emoji
|
||||||
|
chars = [...text];
|
||||||
|
}
|
||||||
|
|
||||||
// Measure total width for centering
|
// Measure total width for centering
|
||||||
const totalWidth = context.measureText(text).width;
|
const totalWidth = context.measureText(text).width;
|
||||||
let currentX = baseX - totalWidth / 2;
|
let currentX = baseX - totalWidth / 2;
|
||||||
|
|
||||||
// Draw each character with wave offset
|
// Draw each character with wave offset
|
||||||
for (let i = 0; i < text.length; i++) {
|
for (let i = 0; i < chars.length; i++) {
|
||||||
const char = text[i];
|
const char = chars[i];
|
||||||
const charWidth = context.measureText(char).width;
|
const charWidth = context.measureText(char).width;
|
||||||
|
|
||||||
// Calculate wave offset for this character
|
// Calculate wave offset for this character
|
||||||
const phase = animState.getPhase(speed);
|
const phase = animState.getPhase(speed);
|
||||||
const charOffset = i / text.length;
|
const charOffset = i / chars.length;
|
||||||
const waveY = Math.sin(phase + charOffset * Math.PI * 2) * amplitude;
|
const waveY = Math.sin(phase + charOffset * Math.PI * 2) * amplitude;
|
||||||
|
|
||||||
const charX = currentX + charWidth / 2;
|
const charX = currentX + charWidth / 2;
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,13 @@ import * as textureBg from "./effects/background-texture.js";
|
||||||
import * as emojiWallpaper from "./effects/background-emoji-wallpaper.js";
|
import * as emojiWallpaper from "./effects/background-emoji-wallpaper.js";
|
||||||
import * as rainbowBg from "./effects/background-rainbow.js";
|
import * as rainbowBg from "./effects/background-rainbow.js";
|
||||||
import * as rain from "./effects/background-rain.js";
|
import * as rain from "./effects/background-rain.js";
|
||||||
|
import * as starfield from "./effects/background-starfield.js";
|
||||||
|
//import * as bubbles from "./effects/background-bubbles.js";
|
||||||
|
import * as aurora from "./effects/background-aurora.js";
|
||||||
|
import * as fire from "./effects/background-fire.js";
|
||||||
import * as border from "./effects/border.js";
|
import * as border from "./effects/border.js";
|
||||||
import * as standardText from "./effects/text-standard.js";
|
import * as standardText from "./effects/text-standard.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 spinText from "./effects/spin-text.js";
|
import * as spinText from "./effects/spin-text.js";
|
||||||
|
|
@ -63,6 +68,7 @@ async function setupApp() {
|
||||||
fps: 20,
|
fps: 20,
|
||||||
duration: 2,
|
duration: 2,
|
||||||
fonts: [
|
fonts: [
|
||||||
|
"Arial",
|
||||||
"Lato",
|
"Lato",
|
||||||
"Roboto",
|
"Roboto",
|
||||||
"Open Sans",
|
"Open Sans",
|
||||||
|
|
@ -84,8 +90,13 @@ async function setupApp() {
|
||||||
emojiWallpaper.register(generator);
|
emojiWallpaper.register(generator);
|
||||||
rainbowBg.register(generator);
|
rainbowBg.register(generator);
|
||||||
rain.register(generator);
|
rain.register(generator);
|
||||||
|
starfield.register(generator);
|
||||||
|
//bubbles.register(generator);
|
||||||
|
aurora.register(generator);
|
||||||
|
fire.register(generator);
|
||||||
border.register(generator);
|
border.register(generator);
|
||||||
standardText.register(generator);
|
standardText.register(generator);
|
||||||
|
textShadow.register(generator);
|
||||||
waveText.register(generator);
|
waveText.register(generator);
|
||||||
rainbowText.register(generator);
|
rainbowText.register(generator);
|
||||||
spinText.register(generator);
|
spinText.register(generator);
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,120 @@ export class UIBuilder {
|
||||||
constructor(containerElement) {
|
constructor(containerElement) {
|
||||||
this.container = containerElement;
|
this.container = containerElement;
|
||||||
this.controlGroups = new Map(); // category -> { element, controls }
|
this.controlGroups = new Map(); // category -> { element, controls }
|
||||||
|
this.tooltip = null;
|
||||||
|
this.tooltipTimeout = null;
|
||||||
|
this.setupTooltip();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create and setup the tooltip element
|
||||||
|
*/
|
||||||
|
setupTooltip() {
|
||||||
|
// Wait for DOM to be ready
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
this.createTooltipElement();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.createTooltipElement();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createTooltipElement() {
|
||||||
|
this.tooltip = document.createElement('div');
|
||||||
|
this.tooltip.className = 'control-tooltip';
|
||||||
|
this.tooltip.style.cssText = `
|
||||||
|
position: fixed;
|
||||||
|
background: linear-gradient(135deg, rgba(0, 120, 200, 0.98) 0%, rgba(0, 100, 180, 0.98) 100%);
|
||||||
|
color: #fff;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 10000;
|
||||||
|
max-width: 250px;
|
||||||
|
box-shadow: 0 0 20px rgba(0, 150, 255, 0.5), 0 4px 12px rgba(0, 0, 0, 0.4);
|
||||||
|
border: 1px solid rgba(0, 150, 255, 0.6);
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.15s ease;
|
||||||
|
line-height: 1.4;
|
||||||
|
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
|
||||||
|
`;
|
||||||
|
document.body.appendChild(this.tooltip);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show tooltip for an element
|
||||||
|
*/
|
||||||
|
showTooltip(element, text) {
|
||||||
|
if (!text || !this.tooltip) return;
|
||||||
|
|
||||||
|
clearTimeout(this.tooltipTimeout);
|
||||||
|
|
||||||
|
this.tooltip.textContent = text;
|
||||||
|
this.tooltip.style.opacity = '1';
|
||||||
|
|
||||||
|
// Position tooltip above the element
|
||||||
|
const rect = element.getBoundingClientRect();
|
||||||
|
|
||||||
|
// Set initial position to measure
|
||||||
|
this.tooltip.style.left = '0px';
|
||||||
|
this.tooltip.style.top = '0px';
|
||||||
|
this.tooltip.style.visibility = 'hidden';
|
||||||
|
this.tooltip.style.display = 'block';
|
||||||
|
|
||||||
|
const tooltipRect = this.tooltip.getBoundingClientRect();
|
||||||
|
|
||||||
|
this.tooltip.style.visibility = 'visible';
|
||||||
|
|
||||||
|
let left = rect.left + rect.width / 2 - tooltipRect.width / 2;
|
||||||
|
let top = rect.top - tooltipRect.height - 10;
|
||||||
|
|
||||||
|
// Keep tooltip on screen
|
||||||
|
const padding = 10;
|
||||||
|
if (left < padding) left = padding;
|
||||||
|
if (left + tooltipRect.width > window.innerWidth - padding) {
|
||||||
|
left = window.innerWidth - tooltipRect.width - padding;
|
||||||
|
}
|
||||||
|
if (top < padding) {
|
||||||
|
top = rect.bottom + 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.tooltip.style.left = `${left}px`;
|
||||||
|
this.tooltip.style.top = `${top}px`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hide tooltip
|
||||||
|
*/
|
||||||
|
hideTooltip() {
|
||||||
|
if (!this.tooltip) return;
|
||||||
|
clearTimeout(this.tooltipTimeout);
|
||||||
|
this.tooltipTimeout = setTimeout(() => {
|
||||||
|
this.tooltip.style.opacity = '0';
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add tooltip handlers to an element
|
||||||
|
*/
|
||||||
|
addTooltipHandlers(element, description) {
|
||||||
|
if (!description) return;
|
||||||
|
|
||||||
|
element.addEventListener('mouseenter', () => {
|
||||||
|
this.showTooltip(element, description);
|
||||||
|
});
|
||||||
|
|
||||||
|
element.addEventListener('mouseleave', () => {
|
||||||
|
this.hideTooltip();
|
||||||
|
});
|
||||||
|
|
||||||
|
element.addEventListener('mousemove', () => {
|
||||||
|
// Update position on mouse move for better following
|
||||||
|
if (this.tooltip && this.tooltip.style.opacity === '1') {
|
||||||
|
this.showTooltip(element, description);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -133,19 +247,19 @@ export class UIBuilder {
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'checkbox':
|
case 'checkbox':
|
||||||
return this.createCheckbox(id, label, defaultValue, showWhen);
|
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 'color':
|
||||||
return this.createColor(id, label, defaultValue, showWhen);
|
return this.createColor(id, label, defaultValue, showWhen, description);
|
||||||
|
|
||||||
case 'select':
|
case 'select':
|
||||||
return this.createSelect(id, label, defaultValue, options, showWhen);
|
return this.createSelect(id, label, defaultValue, options, showWhen, description);
|
||||||
|
|
||||||
case 'text':
|
case 'text':
|
||||||
return this.createTextInput(id, label, defaultValue);
|
return this.createTextInput(id, label, defaultValue, showWhen, description);
|
||||||
|
|
||||||
default:
|
default:
|
||||||
console.warn(`Unknown control type: ${type}`);
|
console.warn(`Unknown control type: ${type}`);
|
||||||
|
|
@ -156,7 +270,7 @@ export class UIBuilder {
|
||||||
/**
|
/**
|
||||||
* Create a checkbox control
|
* Create a checkbox control
|
||||||
*/
|
*/
|
||||||
createCheckbox(id, label, defaultValue, showWhen) {
|
createCheckbox(id, label, defaultValue, showWhen, description) {
|
||||||
const wrapper = document.createElement('label');
|
const wrapper = document.createElement('label');
|
||||||
wrapper.className = 'checkbox-label';
|
wrapper.className = 'checkbox-label';
|
||||||
|
|
||||||
|
|
@ -176,6 +290,9 @@ export class UIBuilder {
|
||||||
wrapper.dataset.showWhen = showWhen;
|
wrapper.dataset.showWhen = showWhen;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add tooltip handlers to the label wrapper
|
||||||
|
this.addTooltipHandlers(wrapper, description);
|
||||||
|
|
||||||
return wrapper;
|
return wrapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -188,9 +305,6 @@ export class UIBuilder {
|
||||||
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>`;
|
||||||
if (description) {
|
|
||||||
labelEl.title = description;
|
|
||||||
}
|
|
||||||
|
|
||||||
const input = document.createElement('input');
|
const input = document.createElement('input');
|
||||||
input.type = 'range';
|
input.type = 'range';
|
||||||
|
|
@ -218,13 +332,16 @@ export class UIBuilder {
|
||||||
container.dataset.showWhen = showWhen;
|
container.dataset.showWhen = showWhen;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add tooltip handlers to the label
|
||||||
|
this.addTooltipHandlers(labelEl, description);
|
||||||
|
|
||||||
return container;
|
return container;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a color picker control
|
* Create a color picker control
|
||||||
*/
|
*/
|
||||||
createColor(id, label, defaultValue, showWhen) {
|
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');
|
||||||
|
|
@ -244,13 +361,16 @@ export class UIBuilder {
|
||||||
container.dataset.showWhen = showWhen;
|
container.dataset.showWhen = showWhen;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add tooltip handlers to the label
|
||||||
|
this.addTooltipHandlers(labelEl, description);
|
||||||
|
|
||||||
return container;
|
return container;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a select dropdown control
|
* Create a select dropdown control
|
||||||
*/
|
*/
|
||||||
createSelect(id, label, defaultValue, options, showWhen) {
|
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');
|
||||||
|
|
@ -278,13 +398,16 @@ export class UIBuilder {
|
||||||
container.dataset.showWhen = showWhen;
|
container.dataset.showWhen = showWhen;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add tooltip handlers to the label
|
||||||
|
this.addTooltipHandlers(labelEl, description);
|
||||||
|
|
||||||
return container;
|
return container;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a text input control
|
* Create a text input control
|
||||||
*/
|
*/
|
||||||
createTextInput(id, label, defaultValue) {
|
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');
|
||||||
|
|
@ -300,6 +423,14 @@ export class UIBuilder {
|
||||||
container.appendChild(labelEl);
|
container.appendChild(labelEl);
|
||||||
container.appendChild(input);
|
container.appendChild(input);
|
||||||
|
|
||||||
|
if (showWhen) {
|
||||||
|
container.style.display = 'none';
|
||||||
|
container.dataset.showWhen = showWhen;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add tooltip handlers to the label
|
||||||
|
this.addTooltipHandlers(labelEl, description);
|
||||||
|
|
||||||
return container;
|
return container;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -348,6 +479,14 @@ export class UIBuilder {
|
||||||
} else if (controlId && controlId.startsWith('text2-gradient-')) {
|
} else if (controlId && controlId.startsWith('text2-gradient-')) {
|
||||||
control.style.display = triggerControl.value === 'gradient' ? 'block' : 'none';
|
control.style.display = triggerControl.value === 'gradient' ? 'block' : 'none';
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
// For border style controls
|
||||||
|
else if (triggerControlId === 'border-style') {
|
||||||
|
if (controlId === 'border-rainbow-speed') {
|
||||||
|
control.style.display = triggerControl.value === 'rainbow' ? 'block' : 'none';
|
||||||
|
} else if (controlId === 'border-march-speed') {
|
||||||
|
control.style.display = triggerControl.value === 'marching-ants' ? 'block' : 'none';
|
||||||
|
}
|
||||||
} else {
|
} 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';
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue