Merge branch 'feature/button-generator'
This commit is contained in:
commit
7b74e5e997
36 changed files with 5822 additions and 591 deletions
|
|
@ -76,7 +76,6 @@
|
||||||
z-index: 90;
|
z-index: 90;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
content: "";
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -37,8 +37,7 @@
|
||||||
<!-- Button Generator - only load if page content contains button-generator shortcode -->
|
<!-- Button Generator - only load if page content contains button-generator shortcode -->
|
||||||
{{ if or (findRE "{{<\\s*button-generator" .RawContent) (findRE "{{%\\s*button-generator" .RawContent) }}
|
{{ if or (findRE "{{<\\s*button-generator" .RawContent) (findRE "{{%\\s*button-generator" .RawContent) }}
|
||||||
<script src="{{ "js/gif.js" | relURL }}"></script>
|
<script src="{{ "js/gif.js" | relURL }}"></script>
|
||||||
{{ $buttonGenerator := resources.Get "js/button-generator.js" | resources.Minify | resources.Fingerprint }}
|
<script type="module" src="{{ "js/button-generator/main.js" | relURL }}"></script>
|
||||||
<script src="{{ $buttonGenerator.RelPermalink }}" integrity="{{ $buttonGenerator.Data.Integrity }}"></script>
|
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -17,591 +17,6 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="controls-section">
|
<div class="controls-section"></div>
|
||||||
<div class="control-group">
|
|
||||||
<h3 class="control-group-header">
|
|
||||||
<span>Text Line 1</span>
|
|
||||||
<span class="toggle-icon">−</span>
|
|
||||||
</h3>
|
|
||||||
<div class="control-group-content">
|
|
||||||
<label for="button-text">Text</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="button-text"
|
|
||||||
value="RITUAL.SH"
|
|
||||||
maxlength="20"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<label class="checkbox-label">
|
|
||||||
<input type="checkbox" id="text-enabled" checked />
|
|
||||||
<span>Enable Text Line 1</span>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label for="font-size"
|
|
||||||
>Font Size: <span id="font-size-value">14</span>px</label
|
|
||||||
>
|
|
||||||
<input type="range" id="font-size" min="6" max="24" value="14" />
|
|
||||||
|
|
||||||
<label for="text-x"
|
|
||||||
>Horizontal Position: <span id="text-x-value">50</span>%</label
|
|
||||||
>
|
|
||||||
<input type="range" id="text-x" min="0" max="100" value="50" />
|
|
||||||
|
|
||||||
<label for="text-y"
|
|
||||||
>Vertical Position: <span id="text-y-value">35</span>%</label
|
|
||||||
>
|
|
||||||
<input type="range" id="text-y" min="0" max="100" value="35" />
|
|
||||||
|
|
||||||
<label for="text-color-type">Text Color Type</label>
|
|
||||||
<select id="text-color-type">
|
|
||||||
<option value="solid">Solid Color</option>
|
|
||||||
<option value="gradient">Gradient</option>
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<div id="text-solid-color">
|
|
||||||
<label for="text-color">Text Color</label>
|
|
||||||
<input type="color" id="text-color" value="#ffffff" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="text-gradient-color" style="display: none">
|
|
||||||
<label for="text-gradient-color1">Gradient Color 1</label>
|
|
||||||
<input type="color" id="text-gradient-color1" value="#ffffff" />
|
|
||||||
|
|
||||||
<label for="text-gradient-color2">Gradient Color 2</label>
|
|
||||||
<input type="color" id="text-gradient-color2" value="#00ffff" />
|
|
||||||
|
|
||||||
<label for="text-gradient-angle"
|
|
||||||
>Gradient Angle:
|
|
||||||
<span id="text-gradient-angle-value">0</span>°</label
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
id="text-gradient-angle"
|
|
||||||
min="0"
|
|
||||||
max="360"
|
|
||||||
value="0"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<label class="checkbox-label">
|
|
||||||
<input type="checkbox" id="text-outline" />
|
|
||||||
<span>Outline</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="color"
|
|
||||||
id="outline-color"
|
|
||||||
value="#000000"
|
|
||||||
style="display: none"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<label for="font-family">Font</label>
|
|
||||||
<select id="font-family">
|
|
||||||
<option value="Lato">Lato</option>
|
|
||||||
<option value="Roboto">Roboto</option>
|
|
||||||
<option value="Open Sans">Open Sans</option>
|
|
||||||
<option value="Montserrat">Montserrat</option>
|
|
||||||
<option value="Oswald">Oswald</option>
|
|
||||||
<option value="Bebas Neue">Bebas Neue</option>
|
|
||||||
<option value="Roboto Mono">Roboto Mono</option>
|
|
||||||
<option value="VT323">VT323</option>
|
|
||||||
<option value="Press Start 2P">Press Start 2P</option>
|
|
||||||
<option value="DSEG7-Classic">DSEG7</option>
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<div class="checkbox-row">
|
|
||||||
<label class="checkbox-label">
|
|
||||||
<input type="checkbox" id="font-bold" />
|
|
||||||
<span>Bold</span>
|
|
||||||
</label>
|
|
||||||
<label class="checkbox-label">
|
|
||||||
<input type="checkbox" id="font-italic" />
|
|
||||||
<span>Italic</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="control-group">
|
|
||||||
<h3 class="control-group-header">
|
|
||||||
<span>Text Line 1 Animation</span>
|
|
||||||
<span class="toggle-icon">−</span>
|
|
||||||
</h3>
|
|
||||||
<div class="control-group-content">
|
|
||||||
<label class="checkbox-label">
|
|
||||||
<input type="checkbox" id="animate-text-wave" />
|
|
||||||
<span>Wave Animation</span>
|
|
||||||
</label>
|
|
||||||
<div id="wave-controls" style="display: none">
|
|
||||||
<label for="wave-amplitude"
|
|
||||||
>Amplitude: <span id="wave-amplitude-value">3</span>px</label
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
id="wave-amplitude"
|
|
||||||
min="0"
|
|
||||||
max="10"
|
|
||||||
value="3"
|
|
||||||
step="0.5"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<label for="wave-speed"
|
|
||||||
>Speed: <span id="wave-speed-value">1.0</span>x</label
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
id="wave-speed"
|
|
||||||
min="0.5"
|
|
||||||
max="3"
|
|
||||||
value="1.0"
|
|
||||||
step="0.1"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<label class="checkbox-label">
|
|
||||||
<input type="checkbox" id="animate-text-rainbow" />
|
|
||||||
<span>Rainbow Text</span>
|
|
||||||
</label>
|
|
||||||
<div id="rainbow-text-controls" style="display: none">
|
|
||||||
<label for="text-rainbow-speed"
|
|
||||||
>Speed: <span id="text-rainbow-speed-value">1.0</span>x</label
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
id="text-rainbow-speed"
|
|
||||||
min="0.5"
|
|
||||||
max="5"
|
|
||||||
value="1.0"
|
|
||||||
step="0.1"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="control-group">
|
|
||||||
<h3 class="control-group-header">
|
|
||||||
<span>Text Line 2</span>
|
|
||||||
<span class="toggle-icon">−</span>
|
|
||||||
</h3>
|
|
||||||
<div class="control-group-content">
|
|
||||||
<label for="button-text2">Text</label>
|
|
||||||
<input type="text" id="button-text2" value="" maxlength="20" />
|
|
||||||
|
|
||||||
<label class="checkbox-label">
|
|
||||||
<input type="checkbox" id="text2-enabled" />
|
|
||||||
<span>Enable Text Line 2</span>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label for="font-size2"
|
|
||||||
>Font Size: <span id="font-size2-value">12</span>px</label
|
|
||||||
>
|
|
||||||
<input type="range" id="font-size2" min="6" max="24" value="12" />
|
|
||||||
|
|
||||||
<label for="text2-x"
|
|
||||||
>Horizontal Position: <span id="text2-x-value">50</span>%</label
|
|
||||||
>
|
|
||||||
<input type="range" id="text2-x" min="0" max="100" value="50" />
|
|
||||||
|
|
||||||
<label for="text2-y"
|
|
||||||
>Vertical Position: <span id="text2-y-value">65</span>%</label
|
|
||||||
>
|
|
||||||
<input type="range" id="text2-y" min="0" max="100" value="65" />
|
|
||||||
|
|
||||||
<label for="text2-color-type">Text Color Type</label>
|
|
||||||
<select id="text2-color-type">
|
|
||||||
<option value="solid">Solid Color</option>
|
|
||||||
<option value="gradient">Gradient</option>
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<div id="text2-solid-color">
|
|
||||||
<label for="text2-color">Text Color</label>
|
|
||||||
<input type="color" id="text2-color" value="#ffffff" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="text2-gradient-color" style="display: none">
|
|
||||||
<label for="text2-gradient-color1">Gradient Color 1</label>
|
|
||||||
<input type="color" id="text2-gradient-color1" value="#ffffff" />
|
|
||||||
|
|
||||||
<label for="text2-gradient-color2">Gradient Color 2</label>
|
|
||||||
<input type="color" id="text2-gradient-color2" value="#00ffff" />
|
|
||||||
|
|
||||||
<label for="text2-gradient-angle"
|
|
||||||
>Gradient Angle:
|
|
||||||
<span id="text2-gradient-angle-value">0</span>°</label
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
id="text2-gradient-angle"
|
|
||||||
min="0"
|
|
||||||
max="360"
|
|
||||||
value="0"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<label class="checkbox-label">
|
|
||||||
<input type="checkbox" id="text2-outline" />
|
|
||||||
<span>Outline</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="color"
|
|
||||||
id="outline2-color"
|
|
||||||
value="#000000"
|
|
||||||
style="display: none"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<label for="font-family2">Font</label>
|
|
||||||
<select id="font-family2">
|
|
||||||
<option value="Lato">Lato</option>
|
|
||||||
<option value="Roboto">Roboto</option>
|
|
||||||
<option value="Open Sans">Open Sans</option>
|
|
||||||
<option value="Montserrat">Montserrat</option>
|
|
||||||
<option value="Oswald">Oswald</option>
|
|
||||||
<option value="Bebas Neue">Bebas Neue</option>
|
|
||||||
<option value="Roboto Mono">Roboto Mono</option>
|
|
||||||
<option value="VT323">VT323</option>
|
|
||||||
<option value="Press Start 2P">Press Start 2P</option>
|
|
||||||
<option value="DSEG7-Classic">DSEG7</option>
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<div class="checkbox-row">
|
|
||||||
<label class="checkbox-label">
|
|
||||||
<input type="checkbox" id="font-bold2" />
|
|
||||||
<span>Bold</span>
|
|
||||||
</label>
|
|
||||||
<label class="checkbox-label">
|
|
||||||
<input type="checkbox" id="font-italic2" />
|
|
||||||
<span>Italic</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="control-group">
|
|
||||||
<h3 class="control-group-header">
|
|
||||||
<span>Text Line 2 Animation</span>
|
|
||||||
<span class="toggle-icon">−</span>
|
|
||||||
</h3>
|
|
||||||
<div class="control-group-content">
|
|
||||||
<label class="checkbox-label">
|
|
||||||
<input type="checkbox" id="animate-text-wave2" />
|
|
||||||
<span>Wave Animation</span>
|
|
||||||
</label>
|
|
||||||
<div id="wave-controls2" style="display: none">
|
|
||||||
<label for="wave-amplitude2"
|
|
||||||
>Amplitude: <span id="wave-amplitude2-value">3</span>px</label
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
id="wave-amplitude2"
|
|
||||||
min="0"
|
|
||||||
max="10"
|
|
||||||
value="3"
|
|
||||||
step="0.5"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<label for="wave-speed2"
|
|
||||||
>Speed: <span id="wave-speed2-value">1.0</span>x</label
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
id="wave-speed2"
|
|
||||||
min="0.5"
|
|
||||||
max="3"
|
|
||||||
value="1.0"
|
|
||||||
step="0.1"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<label class="checkbox-label">
|
|
||||||
<input type="checkbox" id="animate-text-rainbow2" />
|
|
||||||
<span>Rainbow Text</span>
|
|
||||||
</label>
|
|
||||||
<div id="rainbow-text2-controls" style="display: none">
|
|
||||||
<label for="text-rainbow-speed2"
|
|
||||||
>Speed: <span id="text-rainbow-speed2-value">1.0</span>x</label
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
id="text-rainbow-speed2"
|
|
||||||
min="0.5"
|
|
||||||
max="5"
|
|
||||||
value="1.0"
|
|
||||||
step="0.1"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="control-group">
|
|
||||||
<h3 class="control-group-header">
|
|
||||||
<span>Background</span>
|
|
||||||
<span class="toggle-icon">−</span>
|
|
||||||
</h3>
|
|
||||||
<div class="control-group-content">
|
|
||||||
<label for="bg-type">Background Type</label>
|
|
||||||
<select id="bg-type">
|
|
||||||
<option value="solid">Solid Color</option>
|
|
||||||
<option value="gradient">Gradient</option>
|
|
||||||
<option value="texture">Texture</option>
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<div id="solid-controls">
|
|
||||||
<label for="bg-color">Background Color</label>
|
|
||||||
<input type="color" id="bg-color" value="#0066cc" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="gradient-controls" style="display: none">
|
|
||||||
<label for="gradient-color1">Color 1</label>
|
|
||||||
<input type="color" id="gradient-color1" value="#0066cc" />
|
|
||||||
|
|
||||||
<label for="gradient-color2">Color 2</label>
|
|
||||||
<input type="color" id="gradient-color2" value="#00ccff" />
|
|
||||||
|
|
||||||
<label for="gradient-angle"
|
|
||||||
>Angle: <span id="gradient-angle-value">90</span>°</label
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
id="gradient-angle"
|
|
||||||
min="0"
|
|
||||||
max="360"
|
|
||||||
value="90"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="texture-controls" style="display: none">
|
|
||||||
<label for="texture-type">Texture Pattern</label>
|
|
||||||
<select id="texture-type">
|
|
||||||
<option value="dots">Dots</option>
|
|
||||||
<option value="grid">Grid</option>
|
|
||||||
<option value="diagonal">Diagonal Lines</option>
|
|
||||||
<option value="checkerboard">Checkerboard</option>
|
|
||||||
<option value="noise">Noise</option>
|
|
||||||
<option value="stars">Stars</option>
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<label for="texture-color1">Base Color</label>
|
|
||||||
<input type="color" id="texture-color1" value="#0066cc" />
|
|
||||||
|
|
||||||
<label for="texture-color2">Pattern Color</label>
|
|
||||||
<input type="color" id="texture-color2" value="#0099ff" />
|
|
||||||
|
|
||||||
<label for="texture-scale"
|
|
||||||
>Pattern Scale: <span id="texture-scale-value">50</span>%</label
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
id="texture-scale"
|
|
||||||
min="10"
|
|
||||||
max="100"
|
|
||||||
value="50"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="control-group">
|
|
||||||
<h3 class="control-group-header">
|
|
||||||
<span>Background Animation</span>
|
|
||||||
<span class="toggle-icon">−</span>
|
|
||||||
</h3>
|
|
||||||
<div class="control-group-content">
|
|
||||||
<label class="checkbox-label">
|
|
||||||
<input type="checkbox" id="animate-bg-rainbow" />
|
|
||||||
<span>Rainbow Flash</span>
|
|
||||||
</label>
|
|
||||||
<div id="rainbow-bg-controls" style="display: none">
|
|
||||||
<label for="rainbow-speed"
|
|
||||||
>Speed: <span id="rainbow-speed-value">0.5</span>x</label
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
id="rainbow-speed"
|
|
||||||
min="0.1"
|
|
||||||
max="3"
|
|
||||||
value="0.5"
|
|
||||||
step="0.1"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<label class="checkbox-label">
|
|
||||||
<input type="checkbox" id="animate-bg-rainbow-gradient" />
|
|
||||||
<span>Rainbow Gradient</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="control-group">
|
|
||||||
<h3 class="control-group-header">
|
|
||||||
<span>Border</span>
|
|
||||||
<span class="toggle-icon">−</span>
|
|
||||||
</h3>
|
|
||||||
<div class="control-group-content">
|
|
||||||
<label for="border-width"
|
|
||||||
>Border Width: <span id="border-width-value">2</span>px</label
|
|
||||||
>
|
|
||||||
<input type="range" id="border-width" min="0" max="5" value="2" />
|
|
||||||
|
|
||||||
<label for="border-color">Border Color</label>
|
|
||||||
<input type="color" id="border-color" value="#000000" />
|
|
||||||
|
|
||||||
<label for="border-style">Border Style</label>
|
|
||||||
<select id="border-style">
|
|
||||||
<option value="solid">Solid</option>
|
|
||||||
<option value="inset">Inset (3D)</option>
|
|
||||||
<option value="outset">Outset (3D)</option>
|
|
||||||
<option value="ridge">Ridge</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="control-group">
|
|
||||||
<h3 class="control-group-header">
|
|
||||||
<span>Special Effects</span>
|
|
||||||
<span class="toggle-icon">−</span>
|
|
||||||
</h3>
|
|
||||||
<div class="control-group-content">
|
|
||||||
<p class="info-text">
|
|
||||||
Almost all animations should stack, so pick as many as you want.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<label class="checkbox-label">
|
|
||||||
<input type="checkbox" id="animate-glitch" />
|
|
||||||
<span>Glitch Effect</span>
|
|
||||||
</label>
|
|
||||||
<div id="glitch-controls" style="display: none">
|
|
||||||
<label for="glitch-intensity"
|
|
||||||
>Intensity: <span id="glitch-intensity-value">3</span></label
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
id="glitch-intensity"
|
|
||||||
min="1"
|
|
||||||
max="10"
|
|
||||||
value="3"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<label class="checkbox-label">
|
|
||||||
<input type="checkbox" id="animate-pulse" />
|
|
||||||
<span>Pulse Effect</span>
|
|
||||||
</label>
|
|
||||||
<div id="pulse-controls" style="display: none">
|
|
||||||
<label for="pulse-scale"
|
|
||||||
>Scale: <span id="pulse-scale-value">1.1</span>x</label
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
id="pulse-scale"
|
|
||||||
min="1.0"
|
|
||||||
max="1.3"
|
|
||||||
value="1.1"
|
|
||||||
step="0.05"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<label class="checkbox-label">
|
|
||||||
<input type="checkbox" id="animate-shimmer" />
|
|
||||||
<span>Shimmer Effect</span>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label class="checkbox-label">
|
|
||||||
<input type="checkbox" id="animate-scanline" />
|
|
||||||
<span>Scanline Effect</span>
|
|
||||||
</label>
|
|
||||||
<div id="scanline-controls" style="display: none">
|
|
||||||
<label for="scanline-intensity"
|
|
||||||
>Intensity: <span id="scanline-intensity-value">0.3</span></label
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
id="scanline-intensity"
|
|
||||||
min="0.1"
|
|
||||||
max="1"
|
|
||||||
value="0.3"
|
|
||||||
step="0.1"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<label for="scanline-speed"
|
|
||||||
>Speed: <span id="scanline-speed-value">1.0</span>x</label
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
id="scanline-speed"
|
|
||||||
min="0.5"
|
|
||||||
max="3"
|
|
||||||
value="1.0"
|
|
||||||
step="0.1"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<label class="checkbox-label">
|
|
||||||
<input type="checkbox" id="animate-rgb-split" />
|
|
||||||
<span>RGB Split Effect</span>
|
|
||||||
</label>
|
|
||||||
<div id="rgb-split-controls" style="display: none">
|
|
||||||
<label for="rgb-split-intensity"
|
|
||||||
>Intensity: <span id="rgb-split-intensity-value">2</span>px</label
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
id="rgb-split-intensity"
|
|
||||||
min="1"
|
|
||||||
max="5"
|
|
||||||
value="2"
|
|
||||||
step="0.5"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<label class="checkbox-label">
|
|
||||||
<input type="checkbox" id="animate-noise" />
|
|
||||||
<span>Noise Effect</span>
|
|
||||||
</label>
|
|
||||||
<div id="noise-controls" style="display: none">
|
|
||||||
<label for="noise-intensity"
|
|
||||||
>Intensity: <span id="noise-intensity-value">0.1</span></label
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
id="noise-intensity"
|
|
||||||
min="0.05"
|
|
||||||
max="0.5"
|
|
||||||
value="0.1"
|
|
||||||
step="0.05"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{{/*
|
|
||||||
<label class="checkbox-label">
|
|
||||||
<input type="checkbox" id="animate-rotate" />
|
|
||||||
<span>Rotate Effect</span>
|
|
||||||
</label>
|
|
||||||
<div id="rotate-controls" style="display: none">
|
|
||||||
<label for="rotate-angle"
|
|
||||||
>Max Angle: <span id="rotate-angle-value">5</span>°</label
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
id="rotate-angle"
|
|
||||||
min="2"
|
|
||||||
max="15"
|
|
||||||
value="5"
|
|
||||||
step="1"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<label for="rotate-speed"
|
|
||||||
>Speed: <span id="rotate-speed-value">1.0</span>x</label
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
id="rotate-speed"
|
|
||||||
min="0.5"
|
|
||||||
max="3"
|
|
||||||
value="1.0"
|
|
||||||
step="0.1"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
*/}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
607
layouts/shortcodes/button-generator.html.backup
Normal file
607
layouts/shortcodes/button-generator.html.backup
Normal file
|
|
@ -0,0 +1,607 @@
|
||||||
|
<div id="button-generator-app">
|
||||||
|
<div class="generator-container">
|
||||||
|
<div class="preview-section">
|
||||||
|
<h3 class="hidden-md-down">Preview</h3>
|
||||||
|
<div class="preview-container">
|
||||||
|
<div class="preview-wrapper">
|
||||||
|
<canvas id="button-canvas" width="88" height="31"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button id="download-button" class="btn-primary">Download Button</button>
|
||||||
|
|
||||||
|
<div class="presets-container hidden-md-down">
|
||||||
|
<h3>Presets</h3>
|
||||||
|
<button id="preset-random" class="btn-secondary">Random Button</button>
|
||||||
|
<button id="preset-classic" class="btn-secondary">Classic Style</button>
|
||||||
|
<button id="preset-modern" class="btn-secondary">Modern Style</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="controls-section">
|
||||||
|
<div class="control-group">
|
||||||
|
<h3 class="control-group-header">
|
||||||
|
<span>Text Line 1</span>
|
||||||
|
<span class="toggle-icon">−</span>
|
||||||
|
</h3>
|
||||||
|
<div class="control-group-content">
|
||||||
|
<label for="button-text">Text</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="button-text"
|
||||||
|
value="RITUAL.SH"
|
||||||
|
maxlength="20"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<label class="checkbox-label">
|
||||||
|
<input type="checkbox" id="text-enabled" checked />
|
||||||
|
<span>Enable Text Line 1</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label for="font-size"
|
||||||
|
>Font Size: <span id="font-size-value">14</span>px</label
|
||||||
|
>
|
||||||
|
<input type="range" id="font-size" min="6" max="24" value="14" />
|
||||||
|
|
||||||
|
<label for="text-x"
|
||||||
|
>Horizontal Position: <span id="text-x-value">50</span>%</label
|
||||||
|
>
|
||||||
|
<input type="range" id="text-x" min="0" max="100" value="50" />
|
||||||
|
|
||||||
|
<label for="text-y"
|
||||||
|
>Vertical Position: <span id="text-y-value">35</span>%</label
|
||||||
|
>
|
||||||
|
<input type="range" id="text-y" min="0" max="100" value="35" />
|
||||||
|
|
||||||
|
<label for="text-color-type">Text Color Type</label>
|
||||||
|
<select id="text-color-type">
|
||||||
|
<option value="solid">Solid Color</option>
|
||||||
|
<option value="gradient">Gradient</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<div id="text-solid-color">
|
||||||
|
<label for="text-color">Text Color</label>
|
||||||
|
<input type="color" id="text-color" value="#ffffff" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="text-gradient-color" style="display: none">
|
||||||
|
<label for="text-gradient-color1">Gradient Color 1</label>
|
||||||
|
<input type="color" id="text-gradient-color1" value="#ffffff" />
|
||||||
|
|
||||||
|
<label for="text-gradient-color2">Gradient Color 2</label>
|
||||||
|
<input type="color" id="text-gradient-color2" value="#00ffff" />
|
||||||
|
|
||||||
|
<label for="text-gradient-angle"
|
||||||
|
>Gradient Angle:
|
||||||
|
<span id="text-gradient-angle-value">0</span>°</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
id="text-gradient-angle"
|
||||||
|
min="0"
|
||||||
|
max="360"
|
||||||
|
value="0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label class="checkbox-label">
|
||||||
|
<input type="checkbox" id="text-outline" />
|
||||||
|
<span>Outline</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="color"
|
||||||
|
id="outline-color"
|
||||||
|
value="#000000"
|
||||||
|
style="display: none"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<label for="font-family">Font</label>
|
||||||
|
<select id="font-family">
|
||||||
|
<option value="Lato">Lato</option>
|
||||||
|
<option value="Roboto">Roboto</option>
|
||||||
|
<option value="Open Sans">Open Sans</option>
|
||||||
|
<option value="Montserrat">Montserrat</option>
|
||||||
|
<option value="Oswald">Oswald</option>
|
||||||
|
<option value="Bebas Neue">Bebas Neue</option>
|
||||||
|
<option value="Roboto Mono">Roboto Mono</option>
|
||||||
|
<option value="VT323">VT323</option>
|
||||||
|
<option value="Press Start 2P">Press Start 2P</option>
|
||||||
|
<option value="DSEG7-Classic">DSEG7</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<div class="checkbox-row">
|
||||||
|
<label class="checkbox-label">
|
||||||
|
<input type="checkbox" id="font-bold" />
|
||||||
|
<span>Bold</span>
|
||||||
|
</label>
|
||||||
|
<label class="checkbox-label">
|
||||||
|
<input type="checkbox" id="font-italic" />
|
||||||
|
<span>Italic</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="control-group">
|
||||||
|
<h3 class="control-group-header">
|
||||||
|
<span>Text Line 1 Animation</span>
|
||||||
|
<span class="toggle-icon">−</span>
|
||||||
|
</h3>
|
||||||
|
<div class="control-group-content">
|
||||||
|
<label class="checkbox-label">
|
||||||
|
<input type="checkbox" id="animate-text-wave" />
|
||||||
|
<span>Wave Animation</span>
|
||||||
|
</label>
|
||||||
|
<div id="wave-controls" style="display: none">
|
||||||
|
<label for="wave-amplitude"
|
||||||
|
>Amplitude: <span id="wave-amplitude-value">3</span>px</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
id="wave-amplitude"
|
||||||
|
min="0"
|
||||||
|
max="10"
|
||||||
|
value="3"
|
||||||
|
step="0.5"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<label for="wave-speed"
|
||||||
|
>Speed: <span id="wave-speed-value">1.0</span>x</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
id="wave-speed"
|
||||||
|
min="0.5"
|
||||||
|
max="3"
|
||||||
|
value="1.0"
|
||||||
|
step="0.1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label class="checkbox-label">
|
||||||
|
<input type="checkbox" id="animate-text-rainbow" />
|
||||||
|
<span>Rainbow Text</span>
|
||||||
|
</label>
|
||||||
|
<div id="rainbow-text-controls" style="display: none">
|
||||||
|
<label for="text-rainbow-speed"
|
||||||
|
>Speed: <span id="text-rainbow-speed-value">1.0</span>x</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
id="text-rainbow-speed"
|
||||||
|
min="0.5"
|
||||||
|
max="5"
|
||||||
|
value="1.0"
|
||||||
|
step="0.1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="control-group">
|
||||||
|
<h3 class="control-group-header">
|
||||||
|
<span>Text Line 2</span>
|
||||||
|
<span class="toggle-icon">−</span>
|
||||||
|
</h3>
|
||||||
|
<div class="control-group-content">
|
||||||
|
<label for="button-text2">Text</label>
|
||||||
|
<input type="text" id="button-text2" value="" maxlength="20" />
|
||||||
|
|
||||||
|
<label class="checkbox-label">
|
||||||
|
<input type="checkbox" id="text2-enabled" />
|
||||||
|
<span>Enable Text Line 2</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label for="font-size2"
|
||||||
|
>Font Size: <span id="font-size2-value">12</span>px</label
|
||||||
|
>
|
||||||
|
<input type="range" id="font-size2" min="6" max="24" value="12" />
|
||||||
|
|
||||||
|
<label for="text2-x"
|
||||||
|
>Horizontal Position: <span id="text2-x-value">50</span>%</label
|
||||||
|
>
|
||||||
|
<input type="range" id="text2-x" min="0" max="100" value="50" />
|
||||||
|
|
||||||
|
<label for="text2-y"
|
||||||
|
>Vertical Position: <span id="text2-y-value">65</span>%</label
|
||||||
|
>
|
||||||
|
<input type="range" id="text2-y" min="0" max="100" value="65" />
|
||||||
|
|
||||||
|
<label for="text2-color-type">Text Color Type</label>
|
||||||
|
<select id="text2-color-type">
|
||||||
|
<option value="solid">Solid Color</option>
|
||||||
|
<option value="gradient">Gradient</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<div id="text2-solid-color">
|
||||||
|
<label for="text2-color">Text Color</label>
|
||||||
|
<input type="color" id="text2-color" value="#ffffff" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="text2-gradient-color" style="display: none">
|
||||||
|
<label for="text2-gradient-color1">Gradient Color 1</label>
|
||||||
|
<input type="color" id="text2-gradient-color1" value="#ffffff" />
|
||||||
|
|
||||||
|
<label for="text2-gradient-color2">Gradient Color 2</label>
|
||||||
|
<input type="color" id="text2-gradient-color2" value="#00ffff" />
|
||||||
|
|
||||||
|
<label for="text2-gradient-angle"
|
||||||
|
>Gradient Angle:
|
||||||
|
<span id="text2-gradient-angle-value">0</span>°</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
id="text2-gradient-angle"
|
||||||
|
min="0"
|
||||||
|
max="360"
|
||||||
|
value="0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label class="checkbox-label">
|
||||||
|
<input type="checkbox" id="text2-outline" />
|
||||||
|
<span>Outline</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="color"
|
||||||
|
id="outline2-color"
|
||||||
|
value="#000000"
|
||||||
|
style="display: none"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<label for="font-family2">Font</label>
|
||||||
|
<select id="font-family2">
|
||||||
|
<option value="Lato">Lato</option>
|
||||||
|
<option value="Roboto">Roboto</option>
|
||||||
|
<option value="Open Sans">Open Sans</option>
|
||||||
|
<option value="Montserrat">Montserrat</option>
|
||||||
|
<option value="Oswald">Oswald</option>
|
||||||
|
<option value="Bebas Neue">Bebas Neue</option>
|
||||||
|
<option value="Roboto Mono">Roboto Mono</option>
|
||||||
|
<option value="VT323">VT323</option>
|
||||||
|
<option value="Press Start 2P">Press Start 2P</option>
|
||||||
|
<option value="DSEG7-Classic">DSEG7</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<div class="checkbox-row">
|
||||||
|
<label class="checkbox-label">
|
||||||
|
<input type="checkbox" id="font-bold2" />
|
||||||
|
<span>Bold</span>
|
||||||
|
</label>
|
||||||
|
<label class="checkbox-label">
|
||||||
|
<input type="checkbox" id="font-italic2" />
|
||||||
|
<span>Italic</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="control-group">
|
||||||
|
<h3 class="control-group-header">
|
||||||
|
<span>Text Line 2 Animation</span>
|
||||||
|
<span class="toggle-icon">−</span>
|
||||||
|
</h3>
|
||||||
|
<div class="control-group-content">
|
||||||
|
<label class="checkbox-label">
|
||||||
|
<input type="checkbox" id="animate-text-wave2" />
|
||||||
|
<span>Wave Animation</span>
|
||||||
|
</label>
|
||||||
|
<div id="wave-controls2" style="display: none">
|
||||||
|
<label for="wave-amplitude2"
|
||||||
|
>Amplitude: <span id="wave-amplitude2-value">3</span>px</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
id="wave-amplitude2"
|
||||||
|
min="0"
|
||||||
|
max="10"
|
||||||
|
value="3"
|
||||||
|
step="0.5"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<label for="wave-speed2"
|
||||||
|
>Speed: <span id="wave-speed2-value">1.0</span>x</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
id="wave-speed2"
|
||||||
|
min="0.5"
|
||||||
|
max="3"
|
||||||
|
value="1.0"
|
||||||
|
step="0.1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label class="checkbox-label">
|
||||||
|
<input type="checkbox" id="animate-text-rainbow2" />
|
||||||
|
<span>Rainbow Text</span>
|
||||||
|
</label>
|
||||||
|
<div id="rainbow-text2-controls" style="display: none">
|
||||||
|
<label for="text-rainbow-speed2"
|
||||||
|
>Speed: <span id="text-rainbow-speed2-value">1.0</span>x</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
id="text-rainbow-speed2"
|
||||||
|
min="0.5"
|
||||||
|
max="5"
|
||||||
|
value="1.0"
|
||||||
|
step="0.1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="control-group">
|
||||||
|
<h3 class="control-group-header">
|
||||||
|
<span>Background</span>
|
||||||
|
<span class="toggle-icon">−</span>
|
||||||
|
</h3>
|
||||||
|
<div class="control-group-content">
|
||||||
|
<label for="bg-type">Background Type</label>
|
||||||
|
<select id="bg-type">
|
||||||
|
<option value="solid">Solid Color</option>
|
||||||
|
<option value="gradient">Gradient</option>
|
||||||
|
<option value="texture">Texture</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<div id="solid-controls">
|
||||||
|
<label for="bg-color">Background Color</label>
|
||||||
|
<input type="color" id="bg-color" value="#0066cc" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="gradient-controls" style="display: none">
|
||||||
|
<label for="gradient-color1">Color 1</label>
|
||||||
|
<input type="color" id="gradient-color1" value="#0066cc" />
|
||||||
|
|
||||||
|
<label for="gradient-color2">Color 2</label>
|
||||||
|
<input type="color" id="gradient-color2" value="#00ccff" />
|
||||||
|
|
||||||
|
<label for="gradient-angle"
|
||||||
|
>Angle: <span id="gradient-angle-value">90</span>°</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
id="gradient-angle"
|
||||||
|
min="0"
|
||||||
|
max="360"
|
||||||
|
value="90"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="texture-controls" style="display: none">
|
||||||
|
<label for="texture-type">Texture Pattern</label>
|
||||||
|
<select id="texture-type">
|
||||||
|
<option value="dots">Dots</option>
|
||||||
|
<option value="grid">Grid</option>
|
||||||
|
<option value="diagonal">Diagonal Lines</option>
|
||||||
|
<option value="checkerboard">Checkerboard</option>
|
||||||
|
<option value="noise">Noise</option>
|
||||||
|
<option value="stars">Stars</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<label for="texture-color1">Base Color</label>
|
||||||
|
<input type="color" id="texture-color1" value="#0066cc" />
|
||||||
|
|
||||||
|
<label for="texture-color2">Pattern Color</label>
|
||||||
|
<input type="color" id="texture-color2" value="#0099ff" />
|
||||||
|
|
||||||
|
<label for="texture-scale"
|
||||||
|
>Pattern Scale: <span id="texture-scale-value">50</span>%</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
id="texture-scale"
|
||||||
|
min="10"
|
||||||
|
max="100"
|
||||||
|
value="50"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="control-group">
|
||||||
|
<h3 class="control-group-header">
|
||||||
|
<span>Background Animation</span>
|
||||||
|
<span class="toggle-icon">−</span>
|
||||||
|
</h3>
|
||||||
|
<div class="control-group-content">
|
||||||
|
<label class="checkbox-label">
|
||||||
|
<input type="checkbox" id="animate-bg-rainbow" />
|
||||||
|
<span>Rainbow Flash</span>
|
||||||
|
</label>
|
||||||
|
<div id="rainbow-bg-controls" style="display: none">
|
||||||
|
<label for="rainbow-speed"
|
||||||
|
>Speed: <span id="rainbow-speed-value">0.5</span>x</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
id="rainbow-speed"
|
||||||
|
min="0.1"
|
||||||
|
max="3"
|
||||||
|
value="0.5"
|
||||||
|
step="0.1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label class="checkbox-label">
|
||||||
|
<input type="checkbox" id="animate-bg-rainbow-gradient" />
|
||||||
|
<span>Rainbow Gradient</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="control-group">
|
||||||
|
<h3 class="control-group-header">
|
||||||
|
<span>Border</span>
|
||||||
|
<span class="toggle-icon">−</span>
|
||||||
|
</h3>
|
||||||
|
<div class="control-group-content">
|
||||||
|
<label for="border-width"
|
||||||
|
>Border Width: <span id="border-width-value">2</span>px</label
|
||||||
|
>
|
||||||
|
<input type="range" id="border-width" min="0" max="5" value="2" />
|
||||||
|
|
||||||
|
<label for="border-color">Border Color</label>
|
||||||
|
<input type="color" id="border-color" value="#000000" />
|
||||||
|
|
||||||
|
<label for="border-style">Border Style</label>
|
||||||
|
<select id="border-style">
|
||||||
|
<option value="solid">Solid</option>
|
||||||
|
<option value="inset">Inset (3D)</option>
|
||||||
|
<option value="outset">Outset (3D)</option>
|
||||||
|
<option value="ridge">Ridge</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="control-group">
|
||||||
|
<h3 class="control-group-header">
|
||||||
|
<span>Special Effects</span>
|
||||||
|
<span class="toggle-icon">−</span>
|
||||||
|
</h3>
|
||||||
|
<div class="control-group-content">
|
||||||
|
<p class="info-text">
|
||||||
|
Almost all animations should stack, so pick as many as you want.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<label class="checkbox-label">
|
||||||
|
<input type="checkbox" id="animate-glitch" />
|
||||||
|
<span>Glitch Effect</span>
|
||||||
|
</label>
|
||||||
|
<div id="glitch-controls" style="display: none">
|
||||||
|
<label for="glitch-intensity"
|
||||||
|
>Intensity: <span id="glitch-intensity-value">3</span></label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
id="glitch-intensity"
|
||||||
|
min="1"
|
||||||
|
max="10"
|
||||||
|
value="3"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label class="checkbox-label">
|
||||||
|
<input type="checkbox" id="animate-pulse" />
|
||||||
|
<span>Pulse Effect</span>
|
||||||
|
</label>
|
||||||
|
<div id="pulse-controls" style="display: none">
|
||||||
|
<label for="pulse-scale"
|
||||||
|
>Scale: <span id="pulse-scale-value">1.1</span>x</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
id="pulse-scale"
|
||||||
|
min="1.0"
|
||||||
|
max="1.3"
|
||||||
|
value="1.1"
|
||||||
|
step="0.05"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label class="checkbox-label">
|
||||||
|
<input type="checkbox" id="animate-shimmer" />
|
||||||
|
<span>Shimmer Effect</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="checkbox-label">
|
||||||
|
<input type="checkbox" id="animate-scanline" />
|
||||||
|
<span>Scanline Effect</span>
|
||||||
|
</label>
|
||||||
|
<div id="scanline-controls" style="display: none">
|
||||||
|
<label for="scanline-intensity"
|
||||||
|
>Intensity: <span id="scanline-intensity-value">0.3</span></label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
id="scanline-intensity"
|
||||||
|
min="0.1"
|
||||||
|
max="1"
|
||||||
|
value="0.3"
|
||||||
|
step="0.1"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<label for="scanline-speed"
|
||||||
|
>Speed: <span id="scanline-speed-value">1.0</span>x</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
id="scanline-speed"
|
||||||
|
min="0.5"
|
||||||
|
max="3"
|
||||||
|
value="1.0"
|
||||||
|
step="0.1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label class="checkbox-label">
|
||||||
|
<input type="checkbox" id="animate-rgb-split" />
|
||||||
|
<span>RGB Split Effect</span>
|
||||||
|
</label>
|
||||||
|
<div id="rgb-split-controls" style="display: none">
|
||||||
|
<label for="rgb-split-intensity"
|
||||||
|
>Intensity: <span id="rgb-split-intensity-value">2</span>px</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
id="rgb-split-intensity"
|
||||||
|
min="1"
|
||||||
|
max="5"
|
||||||
|
value="2"
|
||||||
|
step="0.5"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label class="checkbox-label">
|
||||||
|
<input type="checkbox" id="animate-noise" />
|
||||||
|
<span>Noise Effect</span>
|
||||||
|
</label>
|
||||||
|
<div id="noise-controls" style="display: none">
|
||||||
|
<label for="noise-intensity"
|
||||||
|
>Intensity: <span id="noise-intensity-value">0.1</span></label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
id="noise-intensity"
|
||||||
|
min="0.05"
|
||||||
|
max="0.5"
|
||||||
|
value="0.1"
|
||||||
|
step="0.05"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{{/*
|
||||||
|
<label class="checkbox-label">
|
||||||
|
<input type="checkbox" id="animate-rotate" />
|
||||||
|
<span>Rotate Effect</span>
|
||||||
|
</label>
|
||||||
|
<div id="rotate-controls" style="display: none">
|
||||||
|
<label for="rotate-angle"
|
||||||
|
>Max Angle: <span id="rotate-angle-value">5</span>°</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
id="rotate-angle"
|
||||||
|
min="2"
|
||||||
|
max="15"
|
||||||
|
value="5"
|
||||||
|
step="1"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<label for="rotate-speed"
|
||||||
|
>Speed: <span id="rotate-speed-value">1.0</span>x</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
id="rotate-speed"
|
||||||
|
min="0.5"
|
||||||
|
max="3"
|
||||||
|
value="1.0"
|
||||||
|
step="0.1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
*/}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
374
static/js/button-generator/button-generator-core.js
Normal file
374
static/js/button-generator/button-generator-core.js
Normal file
|
|
@ -0,0 +1,374 @@
|
||||||
|
import { ButtonEffect } from './effect-base.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Animation state class - passed to effects for frame-based rendering
|
||||||
|
*/
|
||||||
|
export class AnimationState {
|
||||||
|
constructor(frameNumber = 0, totalFrames = 40, fps = 20) {
|
||||||
|
this.frame = frameNumber;
|
||||||
|
this.totalFrames = totalFrames;
|
||||||
|
this.progress = frameNumber / totalFrames; // 0 to 1
|
||||||
|
this.fps = fps;
|
||||||
|
this.time = (frameNumber / fps) * 1000; // milliseconds
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to get phase for periodic animations (0 to 2π)
|
||||||
|
* @param {number} speed - Speed multiplier
|
||||||
|
* @returns {number} Phase in radians
|
||||||
|
*/
|
||||||
|
getPhase(speed = 1.0) {
|
||||||
|
return this.progress * speed * Math.PI * 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main ButtonGenerator class with effect registry system
|
||||||
|
*/
|
||||||
|
export class ButtonGenerator {
|
||||||
|
constructor(canvas, config = {}) {
|
||||||
|
this.canvas = canvas;
|
||||||
|
this.ctx = canvas.getContext('2d');
|
||||||
|
|
||||||
|
// Animation configuration
|
||||||
|
this.animConfig = {
|
||||||
|
fps: config.fps || 20,
|
||||||
|
duration: config.duration || 2, // seconds
|
||||||
|
get totalFrames() {
|
||||||
|
return this.fps * this.duration;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Effect registry organized by type
|
||||||
|
this.effects = {
|
||||||
|
transform: [],
|
||||||
|
background: [],
|
||||||
|
border: [],
|
||||||
|
text: [],
|
||||||
|
text2: [],
|
||||||
|
general: []
|
||||||
|
};
|
||||||
|
|
||||||
|
// Registered effects by ID for quick lookup
|
||||||
|
this.effectsById = new Map();
|
||||||
|
|
||||||
|
// Control elements cache
|
||||||
|
this.controlElements = {};
|
||||||
|
|
||||||
|
// Animation state
|
||||||
|
this.previewAnimationId = null;
|
||||||
|
|
||||||
|
// Font list for preloading
|
||||||
|
this.fonts = config.fonts || [
|
||||||
|
'Lato', 'Roboto', 'Open Sans', 'Montserrat', 'Oswald',
|
||||||
|
'Bebas Neue', 'Roboto Mono', 'VT323', 'Press Start 2P', 'DSEG7-Classic'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register an effect with the generator
|
||||||
|
* @param {ButtonEffect} effect - Effect instance to register
|
||||||
|
*/
|
||||||
|
registerEffect(effect) {
|
||||||
|
if (!(effect instanceof ButtonEffect)) {
|
||||||
|
throw new Error('Effect must extend ButtonEffect class');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.effectsById.has(effect.id)) {
|
||||||
|
console.warn(`Effect with ID "${effect.id}" is already registered. Skipping.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add to type-specific array
|
||||||
|
const type = effect.type;
|
||||||
|
if (!this.effects[type]) {
|
||||||
|
this.effects[type] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
this.effects[type].push(effect);
|
||||||
|
this.effectsById.set(effect.id, effect);
|
||||||
|
|
||||||
|
// Sort by render order
|
||||||
|
this.effects[type].sort((a, b) => a.renderOrder - b.renderOrder);
|
||||||
|
|
||||||
|
console.log(`Registered effect: ${effect.name} (${effect.id}) [${type}]`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all registered effects
|
||||||
|
* @returns {Array<ButtonEffect>}
|
||||||
|
*/
|
||||||
|
getAllEffects() {
|
||||||
|
return Array.from(this.effectsById.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get effects by type
|
||||||
|
* @param {string} type - Effect type
|
||||||
|
* @returns {Array<ButtonEffect>}
|
||||||
|
*/
|
||||||
|
getEffectsByType(type) {
|
||||||
|
return this.effects[type] || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize and preload fonts
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
|
async preloadFonts() {
|
||||||
|
const fontPromises = this.fonts.flatMap(font => [
|
||||||
|
document.fonts.load(`400 12px "${font}"`),
|
||||||
|
document.fonts.load(`700 12px "${font}"`),
|
||||||
|
document.fonts.load(`italic 400 12px "${font}"`)
|
||||||
|
]);
|
||||||
|
|
||||||
|
await Promise.all(fontPromises);
|
||||||
|
console.log('All fonts loaded for canvas');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current control values from DOM
|
||||||
|
* @returns {Object} Map of control ID to value
|
||||||
|
*/
|
||||||
|
getControlValues() {
|
||||||
|
const values = {};
|
||||||
|
|
||||||
|
// Get all registered control IDs from effects
|
||||||
|
const allControls = new Set();
|
||||||
|
this.getAllEffects().forEach(effect => {
|
||||||
|
effect.controls.forEach(control => {
|
||||||
|
allControls.add(control.id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Read values from DOM
|
||||||
|
allControls.forEach(id => {
|
||||||
|
const element = document.getElementById(id);
|
||||||
|
if (element) {
|
||||||
|
if (element.type === 'checkbox') {
|
||||||
|
values[id] = element.checked;
|
||||||
|
} else if (element.type === 'range' || element.type === 'number') {
|
||||||
|
values[id] = parseFloat(element.value);
|
||||||
|
} else {
|
||||||
|
values[id] = element.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw button with all effects applied
|
||||||
|
* @param {AnimationState} animState - Animation state (null for static)
|
||||||
|
* @param {Object} baseControls - Base button controls (text, colors, etc.)
|
||||||
|
*/
|
||||||
|
draw(animState = null, baseControls = {}) {
|
||||||
|
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||||
|
|
||||||
|
const controlValues = { ...baseControls, ...this.getControlValues() };
|
||||||
|
const renderData = {
|
||||||
|
width: this.canvas.width,
|
||||||
|
height: this.canvas.height,
|
||||||
|
centerX: this.canvas.width / 2,
|
||||||
|
centerY: this.canvas.height / 2
|
||||||
|
};
|
||||||
|
|
||||||
|
// Apply effects in order: transform -> background -> border -> text/text2 -> general
|
||||||
|
const renderOrder = ['transform', 'background', 'border', 'text', 'text2', 'general'];
|
||||||
|
|
||||||
|
// Save context once before transforms
|
||||||
|
this.ctx.save();
|
||||||
|
|
||||||
|
renderOrder.forEach(type => {
|
||||||
|
this.effects[type]?.forEach(effect => {
|
||||||
|
if (effect.canApply(controlValues)) {
|
||||||
|
// Transform effects should NOT be wrapped in save/restore
|
||||||
|
// They need to persist for all subsequent drawing operations
|
||||||
|
if (type !== 'transform') {
|
||||||
|
this.ctx.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
effect.apply(this.ctx, controlValues, animState, renderData);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error applying effect ${effect.id}:`, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type !== 'transform') {
|
||||||
|
this.ctx.restore();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Restore context once after all drawing
|
||||||
|
this.ctx.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if any animations are enabled
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
hasAnimationsEnabled() {
|
||||||
|
const controlValues = this.getControlValues();
|
||||||
|
return this.getAllEffects().some(effect =>
|
||||||
|
effect.type !== 'background' && // Background effects can be static
|
||||||
|
effect.isEnabled(controlValues)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start animated preview loop
|
||||||
|
*/
|
||||||
|
startAnimatedPreview() {
|
||||||
|
this.stopAnimatedPreview();
|
||||||
|
|
||||||
|
let frameNum = 0;
|
||||||
|
let lastFrameTime = performance.now();
|
||||||
|
const frameDelay = 1000 / this.animConfig.fps;
|
||||||
|
|
||||||
|
const animate = (currentTime) => {
|
||||||
|
const elapsed = currentTime - lastFrameTime;
|
||||||
|
|
||||||
|
if (elapsed >= frameDelay) {
|
||||||
|
const animState = new AnimationState(
|
||||||
|
frameNum,
|
||||||
|
this.animConfig.totalFrames,
|
||||||
|
this.animConfig.fps
|
||||||
|
);
|
||||||
|
this.draw(animState);
|
||||||
|
|
||||||
|
frameNum = (frameNum + 1) % this.animConfig.totalFrames;
|
||||||
|
lastFrameTime = currentTime - (elapsed % frameDelay);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.previewAnimationId = requestAnimationFrame(animate);
|
||||||
|
};
|
||||||
|
|
||||||
|
this.previewAnimationId = requestAnimationFrame(animate);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop animated preview
|
||||||
|
*/
|
||||||
|
stopAnimatedPreview() {
|
||||||
|
if (this.previewAnimationId) {
|
||||||
|
cancelAnimationFrame(this.previewAnimationId);
|
||||||
|
this.previewAnimationId = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update preview (static or animated based on settings)
|
||||||
|
*/
|
||||||
|
updatePreview() {
|
||||||
|
if (this.hasAnimationsEnabled()) {
|
||||||
|
this.startAnimatedPreview();
|
||||||
|
} else {
|
||||||
|
this.stopAnimatedPreview();
|
||||||
|
this.draw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export as animated GIF
|
||||||
|
* @param {Function} progressCallback - Called with progress (0-1)
|
||||||
|
* @returns {Promise<Blob>}
|
||||||
|
*/
|
||||||
|
async exportAsGif(progressCallback = null) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
try {
|
||||||
|
// Create temporary canvas for frame generation
|
||||||
|
const frameCanvas = document.createElement('canvas');
|
||||||
|
frameCanvas.width = this.canvas.width;
|
||||||
|
frameCanvas.height = this.canvas.height;
|
||||||
|
const frameCtx = frameCanvas.getContext('2d');
|
||||||
|
|
||||||
|
// Initialize gif.js
|
||||||
|
const gif = new GIF({
|
||||||
|
workers: 2,
|
||||||
|
quality: 10,
|
||||||
|
workerScript: '/js/gif.worker.js',
|
||||||
|
width: this.canvas.width,
|
||||||
|
height: this.canvas.height
|
||||||
|
});
|
||||||
|
|
||||||
|
// Generate frames
|
||||||
|
const totalFrames = this.animConfig.totalFrames;
|
||||||
|
|
||||||
|
const generateFrames = async () => {
|
||||||
|
for (let i = 0; i < totalFrames; i++) {
|
||||||
|
const animState = new AnimationState(i, totalFrames, this.animConfig.fps);
|
||||||
|
|
||||||
|
// Draw to temporary canvas
|
||||||
|
frameCtx.clearRect(0, 0, frameCanvas.width, frameCanvas.height);
|
||||||
|
const tempGenerator = new ButtonGenerator(frameCanvas, {
|
||||||
|
fps: this.animConfig.fps,
|
||||||
|
duration: this.animConfig.duration,
|
||||||
|
fonts: this.fonts
|
||||||
|
});
|
||||||
|
|
||||||
|
// Copy effects
|
||||||
|
this.getAllEffects().forEach(effect => {
|
||||||
|
tempGenerator.registerEffect(effect);
|
||||||
|
});
|
||||||
|
|
||||||
|
tempGenerator.draw(animState);
|
||||||
|
|
||||||
|
gif.addFrame(frameCtx, {
|
||||||
|
delay: 1000 / this.animConfig.fps,
|
||||||
|
copy: true
|
||||||
|
});
|
||||||
|
|
||||||
|
if (progressCallback) {
|
||||||
|
progressCallback(i / totalFrames, 'generating');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Yield to browser every 5 frames
|
||||||
|
if (i % 5 === 0) {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
generateFrames().then(() => {
|
||||||
|
gif.on('finished', (blob) => {
|
||||||
|
resolve(blob);
|
||||||
|
});
|
||||||
|
|
||||||
|
gif.on('progress', (progress) => {
|
||||||
|
if (progressCallback) {
|
||||||
|
progressCallback(progress, 'encoding');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
gif.render();
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bind UI controls to redraw on change
|
||||||
|
*/
|
||||||
|
bindControls() {
|
||||||
|
const allControls = new Set();
|
||||||
|
this.getAllEffects().forEach(effect => {
|
||||||
|
effect.controls.forEach(control => {
|
||||||
|
allControls.add(control.id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
allControls.forEach(id => {
|
||||||
|
const element = document.getElementById(id);
|
||||||
|
if (element) {
|
||||||
|
element.addEventListener('input', () => this.updatePreview());
|
||||||
|
element.addEventListener('change', () => this.updatePreview());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
26
static/js/button-generator/debug-helper.js
Normal file
26
static/js/button-generator/debug-helper.js
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
// Debug helper - add this temporarily to main.js to see what's happening
|
||||||
|
|
||||||
|
export function debugControlValues(generator) {
|
||||||
|
console.log('=== DEBUG: Control Values ===');
|
||||||
|
const values = generator.getControlValues();
|
||||||
|
|
||||||
|
// Text controls
|
||||||
|
console.log('Text controls:');
|
||||||
|
console.log(' button-text:', values['button-text']);
|
||||||
|
console.log(' text-enabled:', values['text-enabled']);
|
||||||
|
console.log(' font-size:', values['font-size']);
|
||||||
|
console.log(' text-x:', values['text-x']);
|
||||||
|
console.log(' text-y:', values['text-y']);
|
||||||
|
|
||||||
|
// Check which effects are enabled
|
||||||
|
console.log('\nEnabled effects:');
|
||||||
|
generator.getAllEffects().forEach(effect => {
|
||||||
|
const enabled = effect.isEnabled(values);
|
||||||
|
if (enabled) {
|
||||||
|
console.log(` ✓ ${effect.name} (${effect.id})`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('\nAll control values:', values);
|
||||||
|
console.log('=== END DEBUG ===');
|
||||||
|
}
|
||||||
92
static/js/button-generator/effect-base.js
Normal file
92
static/js/button-generator/effect-base.js
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
/**
|
||||||
|
* Base class for button generator effects
|
||||||
|
* All effects should extend this class and implement required methods
|
||||||
|
*/
|
||||||
|
export class ButtonEffect {
|
||||||
|
/**
|
||||||
|
* @param {Object} config - Effect configuration
|
||||||
|
* @param {string} config.id - Unique identifier for this effect
|
||||||
|
* @param {string} config.name - Display name for the effect
|
||||||
|
* @param {string} config.type - Effect type: 'text', 'text2', 'background', 'general', 'border'
|
||||||
|
* @param {string} config.category - UI category for grouping effects
|
||||||
|
* @param {number} config.renderOrder - Order in rendering pipeline (lower = earlier)
|
||||||
|
*/
|
||||||
|
constructor(config) {
|
||||||
|
this.config = config; // Store full config for subclasses to access
|
||||||
|
this.id = config.id;
|
||||||
|
this.name = config.name;
|
||||||
|
this.type = config.type; // 'text', 'text2', 'background', 'general', 'border'
|
||||||
|
this.category = config.category;
|
||||||
|
this.renderOrder = config.renderOrder || 100;
|
||||||
|
this.controls = this.defineControls();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define UI controls for this effect
|
||||||
|
* @returns {Array<Object>} Array of control definitions
|
||||||
|
*
|
||||||
|
* Control definition format:
|
||||||
|
* {
|
||||||
|
* id: string, // HTML element ID
|
||||||
|
* type: 'checkbox' | 'range' | 'color' | 'select' | 'text',
|
||||||
|
* label: string, // Display label
|
||||||
|
* defaultValue: any, // Default value
|
||||||
|
* min: number, // For range controls
|
||||||
|
* max: number, // For range controls
|
||||||
|
* step: number, // For range controls
|
||||||
|
* options: Array<{value, label}>, // For select controls
|
||||||
|
* showWhen: string, // ID of checkbox that controls visibility
|
||||||
|
* description: string // Optional tooltip/help text
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
defineControls() {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if this effect is currently enabled
|
||||||
|
* @param {Object} controlValues - Current values of all controls
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
isEnabled(controlValues) {
|
||||||
|
// Default: check for a control with ID pattern 'animate-{effectId}' or '{effectId}-enabled'
|
||||||
|
const enableControl = controlValues[`animate-${this.id}`] ||
|
||||||
|
controlValues[`${this.id}-enabled`];
|
||||||
|
return enableControl === true || enableControl === 'true';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply the effect during rendering
|
||||||
|
* @param {CanvasRenderingContext2D} context - Canvas context to draw on
|
||||||
|
* @param {Object} controlValues - Current values of all controls
|
||||||
|
* @param {AnimationState} animState - Current animation state (null for static)
|
||||||
|
* @param {Object} renderData - Additional render data (text metrics, colors, etc.)
|
||||||
|
*/
|
||||||
|
apply(context, controlValues, animState, renderData) {
|
||||||
|
throw new Error('Effect.apply() must be implemented by subclass');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get control values specific to this effect
|
||||||
|
* @param {Object} allControlValues - All control values
|
||||||
|
* @returns {Object} Only the controls relevant to this effect
|
||||||
|
*/
|
||||||
|
getEffectControls(allControlValues) {
|
||||||
|
const effectControls = {};
|
||||||
|
this.controls.forEach(control => {
|
||||||
|
if (control.id in allControlValues) {
|
||||||
|
effectControls[control.id] = allControlValues[control.id];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return effectControls;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate that this effect can be applied
|
||||||
|
* @param {Object} controlValues - Current values of all controls
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
canApply(controlValues) {
|
||||||
|
return this.isEnabled(controlValues);
|
||||||
|
}
|
||||||
|
}
|
||||||
205
static/js/button-generator/effects/EXAMPLE.js
Normal file
205
static/js/button-generator/effects/EXAMPLE.js
Normal file
|
|
@ -0,0 +1,205 @@
|
||||||
|
/**
|
||||||
|
* EXAMPLE EFFECT
|
||||||
|
*
|
||||||
|
* This is a template for creating new effects.
|
||||||
|
* Copy this file and modify it to create your own custom effects.
|
||||||
|
*
|
||||||
|
* This example creates a "spotlight" effect that highlights a circular area
|
||||||
|
* and darkens the rest of the button.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ButtonEffect } from '../effect-base.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Spotlight Effect
|
||||||
|
* Creates a moving circular spotlight that highlights different areas
|
||||||
|
*/
|
||||||
|
export class SpotlightEffect extends ButtonEffect {
|
||||||
|
constructor() {
|
||||||
|
super({
|
||||||
|
// Unique ID for this effect (used in control IDs)
|
||||||
|
id: 'spotlight',
|
||||||
|
|
||||||
|
// Display name shown in UI
|
||||||
|
name: 'Spotlight',
|
||||||
|
|
||||||
|
// Effect type determines render order category
|
||||||
|
// Options: 'background', 'border', 'text', 'text2', 'general'
|
||||||
|
type: 'general',
|
||||||
|
|
||||||
|
// Category for organizing effects in UI
|
||||||
|
category: 'Visual Effects',
|
||||||
|
|
||||||
|
// Render order within type (lower = earlier)
|
||||||
|
// 1-9: backgrounds, 10-19: borders, 20-29: transforms,
|
||||||
|
// 30-49: text, 50-79: overlays, 80-99: post-processing
|
||||||
|
renderOrder: 60
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define UI controls for this effect
|
||||||
|
* These controls will be automatically bound to the generator
|
||||||
|
*/
|
||||||
|
defineControls() {
|
||||||
|
return [
|
||||||
|
// Main enable/disable checkbox
|
||||||
|
{
|
||||||
|
id: 'animate-spotlight',
|
||||||
|
type: 'checkbox',
|
||||||
|
label: 'Spotlight Effect',
|
||||||
|
defaultValue: false,
|
||||||
|
description: 'Moving circular spotlight'
|
||||||
|
},
|
||||||
|
|
||||||
|
// Spotlight size control
|
||||||
|
{
|
||||||
|
id: 'spotlight-size',
|
||||||
|
type: 'range',
|
||||||
|
label: 'Spotlight Size',
|
||||||
|
defaultValue: 20,
|
||||||
|
min: 10,
|
||||||
|
max: 50,
|
||||||
|
step: 1,
|
||||||
|
showWhen: 'animate-spotlight', // Only show when checkbox is enabled
|
||||||
|
description: 'Radius of the spotlight'
|
||||||
|
},
|
||||||
|
|
||||||
|
// Darkness of the vignette
|
||||||
|
{
|
||||||
|
id: 'spotlight-darkness',
|
||||||
|
type: 'range',
|
||||||
|
label: 'Darkness',
|
||||||
|
defaultValue: 0.5,
|
||||||
|
min: 0,
|
||||||
|
max: 1,
|
||||||
|
step: 0.05,
|
||||||
|
showWhen: 'animate-spotlight',
|
||||||
|
description: 'How dark the non-spotlight area should be'
|
||||||
|
},
|
||||||
|
|
||||||
|
// Speed of movement
|
||||||
|
{
|
||||||
|
id: 'spotlight-speed',
|
||||||
|
type: 'range',
|
||||||
|
label: 'Movement Speed',
|
||||||
|
defaultValue: 1,
|
||||||
|
min: 0.1,
|
||||||
|
max: 3,
|
||||||
|
step: 0.1,
|
||||||
|
showWhen: 'animate-spotlight',
|
||||||
|
description: 'Speed of spotlight movement'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if this effect should be applied
|
||||||
|
* @param {Object} controlValues - Current values of all controls
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
isEnabled(controlValues) {
|
||||||
|
return controlValues['animate-spotlight'] === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply the effect to the canvas
|
||||||
|
*
|
||||||
|
* @param {CanvasRenderingContext2D} context - Canvas 2D rendering context
|
||||||
|
* @param {Object} controlValues - Current values of all controls
|
||||||
|
* @param {AnimationState|null} animState - Animation state (null for static render)
|
||||||
|
* @param {Object} renderData - Render information: { width, height, centerX, centerY }
|
||||||
|
*/
|
||||||
|
apply(context, controlValues, animState, renderData) {
|
||||||
|
// Skip if no animation (spotlight needs movement)
|
||||||
|
if (!animState) return;
|
||||||
|
|
||||||
|
// Get control values
|
||||||
|
const size = controlValues['spotlight-size'] || 20;
|
||||||
|
const darkness = controlValues['spotlight-darkness'] || 0.5;
|
||||||
|
const speed = controlValues['spotlight-speed'] || 1;
|
||||||
|
|
||||||
|
// Calculate spotlight position
|
||||||
|
// Move in a circular pattern using animation phase
|
||||||
|
const phase = animState.getPhase(speed);
|
||||||
|
const spotX = renderData.centerX + Math.cos(phase) * 20;
|
||||||
|
const spotY = renderData.centerY + Math.sin(phase) * 10;
|
||||||
|
|
||||||
|
// Create radial gradient for spotlight effect
|
||||||
|
const gradient = context.createRadialGradient(
|
||||||
|
spotX, spotY, 0, // Inner circle (center of spotlight)
|
||||||
|
spotX, spotY, size // Outer circle (edge of spotlight)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Center is transparent (spotlight is bright)
|
||||||
|
gradient.addColorStop(0, `rgba(0, 0, 0, 0)`);
|
||||||
|
// Edge fades to dark
|
||||||
|
gradient.addColorStop(0.5, `rgba(0, 0, 0, ${darkness * 0.3})`);
|
||||||
|
gradient.addColorStop(1, `rgba(0, 0, 0, ${darkness})`);
|
||||||
|
|
||||||
|
// Apply the gradient as an overlay
|
||||||
|
context.fillStyle = gradient;
|
||||||
|
context.fillRect(0, 0, renderData.width, renderData.height);
|
||||||
|
|
||||||
|
// Optional: Add a bright center dot
|
||||||
|
context.fillStyle = 'rgba(255, 255, 255, 0.3)';
|
||||||
|
context.beginPath();
|
||||||
|
context.arc(spotX, spotY, 2, 0, Math.PI * 2);
|
||||||
|
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
|
||||||
|
*/
|
||||||
|
calculateSpotlightPath(progress, width, height) {
|
||||||
|
// Example helper method
|
||||||
|
return {
|
||||||
|
x: width * progress,
|
||||||
|
y: height / 2
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registration function
|
||||||
|
* This is called to add the effect to the generator
|
||||||
|
*
|
||||||
|
* @param {ButtonGenerator} generator - The button generator instance
|
||||||
|
*/
|
||||||
|
export function register(generator) {
|
||||||
|
generator.registerEffect(new SpotlightEffect());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* USAGE:
|
||||||
|
*
|
||||||
|
* 1. Copy this file to a new name (e.g., my-effect.js)
|
||||||
|
* 2. Modify the class name, id, and effect logic
|
||||||
|
* 3. Import in main.js:
|
||||||
|
* import * as myEffect from './effects/my-effect.js';
|
||||||
|
* 4. Register in setupApp():
|
||||||
|
* myEffect.register(generator);
|
||||||
|
* 5. Add HTML controls with matching IDs
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TIPS:
|
||||||
|
*
|
||||||
|
* - Use animState.progress for linear animations (0 to 1)
|
||||||
|
* - Use animState.getPhase(speed) for periodic animations (0 to 2π)
|
||||||
|
* - Use Math.sin/cos for smooth periodic motion
|
||||||
|
* - Check if (!animState) at the start if your effect requires animation
|
||||||
|
* - The context is automatically saved/restored, so feel free to transform
|
||||||
|
* - Use renderData for canvas dimensions and center point
|
||||||
|
* - Look at existing effects for more examples
|
||||||
|
*/
|
||||||
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());
|
||||||
|
}
|
||||||
109
static/js/button-generator/effects/background-emoji-wallpaper.js
Normal file
109
static/js/button-generator/effects/background-emoji-wallpaper.js
Normal file
|
|
@ -0,0 +1,109 @@
|
||||||
|
import { ButtonEffect } from '../effect-base.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emoji wallpaper background effect
|
||||||
|
* Tiles a user-specified emoji across the background
|
||||||
|
*/
|
||||||
|
export class EmojiWallpaperEffect extends ButtonEffect {
|
||||||
|
constructor() {
|
||||||
|
super({
|
||||||
|
id: 'bg-emoji-wallpaper',
|
||||||
|
name: 'Emoji Wallpaper',
|
||||||
|
type: 'background',
|
||||||
|
category: 'Background',
|
||||||
|
renderOrder: 1
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
defineControls() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: 'emoji-text',
|
||||||
|
type: 'text',
|
||||||
|
label: 'Emoji Character',
|
||||||
|
defaultValue: '✨',
|
||||||
|
showWhen: 'bg-type',
|
||||||
|
description: 'Emoji to tile (can be any text)'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'emoji-size',
|
||||||
|
type: 'range',
|
||||||
|
label: 'Emoji Size',
|
||||||
|
defaultValue: 12,
|
||||||
|
min: 6,
|
||||||
|
max: 24,
|
||||||
|
step: 1,
|
||||||
|
showWhen: 'bg-type',
|
||||||
|
description: 'Size of each emoji'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'emoji-spacing',
|
||||||
|
type: 'range',
|
||||||
|
label: 'Emoji Spacing',
|
||||||
|
defaultValue: 16,
|
||||||
|
min: 8,
|
||||||
|
max: 32,
|
||||||
|
step: 2,
|
||||||
|
showWhen: 'bg-type',
|
||||||
|
description: 'Space between emojis'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'emoji-bg-color',
|
||||||
|
type: 'color',
|
||||||
|
label: 'Background Color',
|
||||||
|
defaultValue: '#1a1a2e',
|
||||||
|
showWhen: 'bg-type',
|
||||||
|
description: 'Background color behind emojis'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'emoji-opacity',
|
||||||
|
type: 'range',
|
||||||
|
label: 'Emoji Opacity',
|
||||||
|
defaultValue: 30,
|
||||||
|
min: 10,
|
||||||
|
max: 100,
|
||||||
|
step: 5,
|
||||||
|
showWhen: 'bg-type',
|
||||||
|
description: 'Transparency of emojis (lower = more transparent)'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
isEnabled(controlValues) {
|
||||||
|
return controlValues['bg-type'] === 'emoji-wallpaper';
|
||||||
|
}
|
||||||
|
|
||||||
|
apply(context, controlValues, animState, renderData) {
|
||||||
|
const emoji = controlValues['emoji-text'] || '✨';
|
||||||
|
const size = controlValues['emoji-size'] || 12;
|
||||||
|
const spacing = controlValues['emoji-spacing'] || 16;
|
||||||
|
const bgColor = controlValues['emoji-bg-color'] || '#1a1a2e';
|
||||||
|
const opacity = (controlValues['emoji-opacity'] || 30) / 100;
|
||||||
|
|
||||||
|
// Fill background color
|
||||||
|
context.fillStyle = bgColor;
|
||||||
|
context.fillRect(0, 0, renderData.width, renderData.height);
|
||||||
|
|
||||||
|
// Setup emoji font
|
||||||
|
context.font = `${size}px Arial`;
|
||||||
|
context.textAlign = 'center';
|
||||||
|
context.textBaseline = 'middle';
|
||||||
|
context.globalAlpha = opacity;
|
||||||
|
|
||||||
|
// Tile emojis
|
||||||
|
for (let y = 0; y < renderData.height + spacing; y += spacing) {
|
||||||
|
for (let x = 0; x < renderData.width + spacing; x += spacing) {
|
||||||
|
// Offset every other row for a brick pattern
|
||||||
|
const offsetX = (Math.floor(y / spacing) % 2) * (spacing / 2);
|
||||||
|
context.fillText(emoji, x + offsetX, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
context.globalAlpha = 1.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-register effect
|
||||||
|
export function register(generator) {
|
||||||
|
generator.registerEffect(new EmojiWallpaperEffect());
|
||||||
|
}
|
||||||
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());
|
||||||
|
}
|
||||||
76
static/js/button-generator/effects/background-gradient.js
Normal file
76
static/js/button-generator/effects/background-gradient.js
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
import { ButtonEffect } from '../effect-base.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gradient background effect
|
||||||
|
*/
|
||||||
|
export class GradientBackgroundEffect extends ButtonEffect {
|
||||||
|
constructor() {
|
||||||
|
super({
|
||||||
|
id: 'bg-gradient',
|
||||||
|
name: 'Gradient Background',
|
||||||
|
type: 'background',
|
||||||
|
category: 'Background',
|
||||||
|
renderOrder: 1
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
defineControls() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: 'gradient-color1',
|
||||||
|
type: 'color',
|
||||||
|
label: 'Gradient Color 1',
|
||||||
|
defaultValue: '#ff0000',
|
||||||
|
showWhen: 'bg-type',
|
||||||
|
description: 'Start color of gradient'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'gradient-color2',
|
||||||
|
type: 'color',
|
||||||
|
label: 'Gradient Color 2',
|
||||||
|
defaultValue: '#0000ff',
|
||||||
|
showWhen: 'bg-type',
|
||||||
|
description: 'End color of gradient'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'gradient-angle',
|
||||||
|
type: 'range',
|
||||||
|
label: 'Gradient Angle',
|
||||||
|
defaultValue: 90,
|
||||||
|
min: 0,
|
||||||
|
max: 360,
|
||||||
|
step: 1,
|
||||||
|
showWhen: 'bg-type',
|
||||||
|
description: 'Angle of gradient direction'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
isEnabled(controlValues) {
|
||||||
|
return controlValues['bg-type'] === 'gradient';
|
||||||
|
}
|
||||||
|
|
||||||
|
apply(context, controlValues, animState, renderData) {
|
||||||
|
const color1 = controlValues['gradient-color1'] || '#ff0000';
|
||||||
|
const color2 = controlValues['gradient-color2'] || '#0000ff';
|
||||||
|
const angle = (controlValues['gradient-angle'] || 90) * (Math.PI / 180);
|
||||||
|
|
||||||
|
// Calculate gradient endpoints
|
||||||
|
const x1 = renderData.centerX + Math.cos(angle) * renderData.centerX;
|
||||||
|
const y1 = renderData.centerY + Math.sin(angle) * renderData.centerY;
|
||||||
|
const x2 = renderData.centerX - Math.cos(angle) * renderData.centerX;
|
||||||
|
const y2 = renderData.centerY - Math.sin(angle) * renderData.centerY;
|
||||||
|
|
||||||
|
const gradient = context.createLinearGradient(x1, y1, x2, y2);
|
||||||
|
gradient.addColorStop(0, color1);
|
||||||
|
gradient.addColorStop(1, color2);
|
||||||
|
|
||||||
|
context.fillStyle = gradient;
|
||||||
|
context.fillRect(0, 0, renderData.width, renderData.height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-register effect
|
||||||
|
export function register(generator) {
|
||||||
|
generator.registerEffect(new GradientBackgroundEffect());
|
||||||
|
}
|
||||||
117
static/js/button-generator/effects/background-rain.js
Normal file
117
static/js/button-generator/effects/background-rain.js
Normal file
|
|
@ -0,0 +1,117 @@
|
||||||
|
import { ButtonEffect } from '../effect-base.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Raining background effect
|
||||||
|
* Animated raindrops falling down the button
|
||||||
|
*/
|
||||||
|
export class RainBackgroundEffect extends ButtonEffect {
|
||||||
|
constructor() {
|
||||||
|
super({
|
||||||
|
id: 'bg-rain',
|
||||||
|
name: 'Rain Effect',
|
||||||
|
type: 'general',
|
||||||
|
category: 'Background Animations',
|
||||||
|
renderOrder: 55 // After background, before other effects
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize raindrop positions (persistent across frames)
|
||||||
|
this.raindrops = [];
|
||||||
|
this.initialized = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
defineControls() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: 'animate-rain',
|
||||||
|
type: 'checkbox',
|
||||||
|
label: 'Rain Effect',
|
||||||
|
defaultValue: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'rain-density',
|
||||||
|
type: 'range',
|
||||||
|
label: 'Rain Density',
|
||||||
|
defaultValue: 15,
|
||||||
|
min: 5,
|
||||||
|
max: 30,
|
||||||
|
step: 1,
|
||||||
|
showWhen: 'animate-rain',
|
||||||
|
description: 'Number of raindrops'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'rain-speed',
|
||||||
|
type: 'range',
|
||||||
|
label: 'Rain Speed',
|
||||||
|
defaultValue: 1.5,
|
||||||
|
min: 0.5,
|
||||||
|
max: 3,
|
||||||
|
step: 0.1,
|
||||||
|
showWhen: 'animate-rain',
|
||||||
|
description: 'Speed of falling rain'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'rain-color',
|
||||||
|
type: 'color',
|
||||||
|
label: 'Rain Color',
|
||||||
|
defaultValue: '#6ba3ff',
|
||||||
|
showWhen: 'animate-rain',
|
||||||
|
description: 'Color of raindrops'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
isEnabled(controlValues) {
|
||||||
|
return controlValues['animate-rain'] === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
apply(context, controlValues, animState, renderData) {
|
||||||
|
if (!animState) return;
|
||||||
|
|
||||||
|
const density = controlValues['rain-density'] || 15;
|
||||||
|
const speed = controlValues['rain-speed'] || 1.5;
|
||||||
|
const color = controlValues['rain-color'] || '#6ba3ff';
|
||||||
|
|
||||||
|
// Initialize raindrops on first frame
|
||||||
|
if (!this.initialized || this.raindrops.length !== density) {
|
||||||
|
this.raindrops = [];
|
||||||
|
for (let i = 0; i < density; i++) {
|
||||||
|
this.raindrops.push({
|
||||||
|
x: Math.random() * renderData.width,
|
||||||
|
y: Math.random() * renderData.height,
|
||||||
|
length: 2 + Math.random() * 4,
|
||||||
|
speedMultiplier: 0.8 + Math.random() * 0.4
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.initialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw raindrops
|
||||||
|
context.strokeStyle = color;
|
||||||
|
context.lineWidth = 1;
|
||||||
|
context.lineCap = 'round';
|
||||||
|
|
||||||
|
this.raindrops.forEach(drop => {
|
||||||
|
// Update position
|
||||||
|
drop.y += speed * drop.speedMultiplier;
|
||||||
|
|
||||||
|
// Reset to top when reaching bottom
|
||||||
|
if (drop.y > renderData.height + drop.length) {
|
||||||
|
drop.y = -drop.length;
|
||||||
|
drop.x = Math.random() * renderData.width;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw raindrop
|
||||||
|
context.globalAlpha = 0.6;
|
||||||
|
context.beginPath();
|
||||||
|
context.moveTo(drop.x, drop.y);
|
||||||
|
context.lineTo(drop.x, drop.y + drop.length);
|
||||||
|
context.stroke();
|
||||||
|
context.globalAlpha = 1.0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-register effect
|
||||||
|
export function register(generator) {
|
||||||
|
generator.registerEffect(new RainBackgroundEffect());
|
||||||
|
}
|
||||||
137
static/js/button-generator/effects/background-rainbow.js
Normal file
137
static/js/button-generator/effects/background-rainbow.js
Normal file
|
|
@ -0,0 +1,137 @@
|
||||||
|
import { ButtonEffect } from '../effect-base.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rainbow flash background effect
|
||||||
|
* Animates background through rainbow colors
|
||||||
|
*/
|
||||||
|
export class RainbowBackgroundEffect extends ButtonEffect {
|
||||||
|
constructor() {
|
||||||
|
super({
|
||||||
|
id: 'bg-rainbow',
|
||||||
|
name: 'Rainbow Background',
|
||||||
|
type: 'background',
|
||||||
|
category: 'Background Animations',
|
||||||
|
renderOrder: 2 // After base background
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
defineControls() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: 'animate-bg-rainbow',
|
||||||
|
type: 'checkbox',
|
||||||
|
label: 'Rainbow Flash',
|
||||||
|
defaultValue: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'rainbow-speed',
|
||||||
|
type: 'range',
|
||||||
|
label: 'Rainbow Speed',
|
||||||
|
defaultValue: 1,
|
||||||
|
min: 0.1,
|
||||||
|
max: 5,
|
||||||
|
step: 0.1,
|
||||||
|
showWhen: 'animate-bg-rainbow',
|
||||||
|
description: 'Speed of rainbow cycling'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
isEnabled(controlValues) {
|
||||||
|
return controlValues['animate-bg-rainbow'] === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
apply(context, controlValues, animState, renderData) {
|
||||||
|
if (!animState) return;
|
||||||
|
|
||||||
|
const speed = controlValues['rainbow-speed'] || 1;
|
||||||
|
const hue = (animState.progress * speed * 360) % 360;
|
||||||
|
|
||||||
|
const bgType = controlValues['bg-type'];
|
||||||
|
|
||||||
|
if (bgType === 'solid') {
|
||||||
|
// Solid rainbow
|
||||||
|
context.fillStyle = `hsl(${hue}, 70%, 50%)`;
|
||||||
|
context.fillRect(0, 0, renderData.width, renderData.height);
|
||||||
|
} else if (bgType === 'gradient') {
|
||||||
|
// Rainbow gradient
|
||||||
|
const angle = (controlValues['gradient-angle'] || 90) * (Math.PI / 180);
|
||||||
|
const x1 = renderData.centerX + Math.cos(angle) * renderData.centerX;
|
||||||
|
const y1 = renderData.centerY + Math.sin(angle) * renderData.centerY;
|
||||||
|
const x2 = renderData.centerX - Math.cos(angle) * renderData.centerX;
|
||||||
|
const y2 = renderData.centerY - Math.sin(angle) * renderData.centerY;
|
||||||
|
|
||||||
|
const gradient = context.createLinearGradient(x1, y1, x2, y2);
|
||||||
|
gradient.addColorStop(0, `hsl(${hue}, 70%, 50%)`);
|
||||||
|
gradient.addColorStop(1, `hsl(${(hue + 60) % 360}, 70%, 60%)`);
|
||||||
|
|
||||||
|
context.fillStyle = gradient;
|
||||||
|
context.fillRect(0, 0, renderData.width, renderData.height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rainbow gradient sweep effect
|
||||||
|
* Creates a moving rainbow gradient that sweeps across the button
|
||||||
|
*/
|
||||||
|
export class RainbowGradientSweepEffect extends ButtonEffect {
|
||||||
|
constructor() {
|
||||||
|
super({
|
||||||
|
id: 'bg-rainbow-gradient',
|
||||||
|
name: 'Rainbow Gradient Sweep',
|
||||||
|
type: 'general',
|
||||||
|
category: 'Background Animations',
|
||||||
|
renderOrder: 50 // After background and text
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
defineControls() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: 'animate-bg-rainbow-gradient',
|
||||||
|
type: 'checkbox',
|
||||||
|
label: 'Rainbow Sweep',
|
||||||
|
defaultValue: false,
|
||||||
|
description: 'Moving rainbow gradient overlay'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
isEnabled(controlValues) {
|
||||||
|
return controlValues['animate-bg-rainbow-gradient'] === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
apply(context, controlValues, animState, renderData) {
|
||||||
|
if (!animState) return;
|
||||||
|
|
||||||
|
// Map progress to position (-100 to 100)
|
||||||
|
const position = animState.progress * 200 - 100;
|
||||||
|
|
||||||
|
// Create a horizontal gradient that sweeps across
|
||||||
|
const gradient = context.createLinearGradient(
|
||||||
|
position - 50,
|
||||||
|
0,
|
||||||
|
position + 50,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create rainbow stops that also cycle through colors
|
||||||
|
const hueOffset = animState.progress * 360;
|
||||||
|
gradient.addColorStop(0, `hsla(${(hueOffset + 0) % 360}, 80%, 50%, 0)`);
|
||||||
|
gradient.addColorStop(0.2, `hsla(${(hueOffset + 60) % 360}, 80%, 50%, 0.6)`);
|
||||||
|
gradient.addColorStop(0.4, `hsla(${(hueOffset + 120) % 360}, 80%, 50%, 0.8)`);
|
||||||
|
gradient.addColorStop(0.6, `hsla(${(hueOffset + 180) % 360}, 80%, 50%, 0.8)`);
|
||||||
|
gradient.addColorStop(0.8, `hsla(${(hueOffset + 240) % 360}, 80%, 50%, 0.6)`);
|
||||||
|
gradient.addColorStop(1, `hsla(${(hueOffset + 300) % 360}, 80%, 50%, 0)`);
|
||||||
|
|
||||||
|
context.fillStyle = gradient;
|
||||||
|
context.fillRect(0, 0, renderData.width, renderData.height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-register effects
|
||||||
|
export function register(generator) {
|
||||||
|
generator.registerEffect(new RainbowBackgroundEffect());
|
||||||
|
generator.registerEffect(new RainbowGradientSweepEffect());
|
||||||
|
}
|
||||||
56
static/js/button-generator/effects/background-solid.js
Normal file
56
static/js/button-generator/effects/background-solid.js
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
import { ButtonEffect } from '../effect-base.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Solid color background effect
|
||||||
|
*/
|
||||||
|
export class SolidBackgroundEffect extends ButtonEffect {
|
||||||
|
constructor() {
|
||||||
|
super({
|
||||||
|
id: 'bg-solid',
|
||||||
|
name: 'Solid Background',
|
||||||
|
type: 'background',
|
||||||
|
category: 'Background',
|
||||||
|
renderOrder: 1
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
defineControls() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: 'bg-type',
|
||||||
|
type: 'select',
|
||||||
|
label: 'Background Type',
|
||||||
|
defaultValue: 'solid',
|
||||||
|
options: [
|
||||||
|
{ value: 'solid', label: 'Solid Color' },
|
||||||
|
{ value: 'gradient', label: 'Gradient' },
|
||||||
|
{ value: 'texture', label: 'Texture' },
|
||||||
|
{ value: 'emoji-wallpaper', label: 'Emoji Wallpaper' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'bg-color',
|
||||||
|
type: 'color',
|
||||||
|
label: 'Background Color',
|
||||||
|
defaultValue: '#4a90e2',
|
||||||
|
showWhen: 'bg-type',
|
||||||
|
description: 'Solid background color'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
isEnabled(controlValues) {
|
||||||
|
return controlValues['bg-type'] === 'solid';
|
||||||
|
}
|
||||||
|
|
||||||
|
apply(context, controlValues, animState, renderData) {
|
||||||
|
const color = controlValues['bg-color'] || '#4a90e2';
|
||||||
|
context.fillStyle = color;
|
||||||
|
context.fillRect(0, 0, renderData.width, renderData.height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-register effect
|
||||||
|
export function register(generator) {
|
||||||
|
generator.registerEffect(new SolidBackgroundEffect());
|
||||||
|
}
|
||||||
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());
|
||||||
|
}
|
||||||
217
static/js/button-generator/effects/background-texture.js
Normal file
217
static/js/button-generator/effects/background-texture.js
Normal file
|
|
@ -0,0 +1,217 @@
|
||||||
|
import { ButtonEffect } from '../effect-base.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Texture background effect
|
||||||
|
* Provides various procedural texture patterns
|
||||||
|
*/
|
||||||
|
export class TextureBackgroundEffect extends ButtonEffect {
|
||||||
|
constructor() {
|
||||||
|
super({
|
||||||
|
id: 'bg-texture',
|
||||||
|
name: 'Texture Background',
|
||||||
|
type: 'background',
|
||||||
|
category: 'Background',
|
||||||
|
renderOrder: 1
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
defineControls() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: 'texture-type',
|
||||||
|
type: 'select',
|
||||||
|
label: 'Texture Type',
|
||||||
|
defaultValue: 'dots',
|
||||||
|
showWhen: 'bg-type',
|
||||||
|
options: [
|
||||||
|
{ value: 'dots', label: 'Dots' },
|
||||||
|
{ value: 'grid', label: 'Grid' },
|
||||||
|
{ value: 'diagonal', label: 'Diagonal Lines' },
|
||||||
|
{ value: 'checkerboard', label: 'Checkerboard' },
|
||||||
|
{ value: 'noise', label: 'Noise' },
|
||||||
|
{ value: 'stars', label: 'Stars' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'texture-color1',
|
||||||
|
type: 'color',
|
||||||
|
label: 'Texture Color 1',
|
||||||
|
defaultValue: '#000000',
|
||||||
|
showWhen: 'bg-type',
|
||||||
|
description: 'Base color'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'texture-color2',
|
||||||
|
type: 'color',
|
||||||
|
label: 'Texture Color 2',
|
||||||
|
defaultValue: '#ffffff',
|
||||||
|
showWhen: 'bg-type',
|
||||||
|
description: 'Pattern color'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'texture-scale',
|
||||||
|
type: 'range',
|
||||||
|
label: 'Texture Scale',
|
||||||
|
defaultValue: 50,
|
||||||
|
min: 10,
|
||||||
|
max: 100,
|
||||||
|
step: 5,
|
||||||
|
showWhen: 'bg-type',
|
||||||
|
description: 'Size/density of pattern'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
isEnabled(controlValues) {
|
||||||
|
return controlValues['bg-type'] === 'texture';
|
||||||
|
}
|
||||||
|
|
||||||
|
apply(context, controlValues, animState, renderData) {
|
||||||
|
const type = controlValues['texture-type'] || 'dots';
|
||||||
|
const color1 = controlValues['texture-color1'] || '#000000';
|
||||||
|
const color2 = controlValues['texture-color2'] || '#ffffff';
|
||||||
|
const scale = controlValues['texture-scale'] || 50;
|
||||||
|
|
||||||
|
const texture = this.drawTexture(
|
||||||
|
type,
|
||||||
|
color1,
|
||||||
|
color2,
|
||||||
|
scale,
|
||||||
|
renderData.width,
|
||||||
|
renderData.height
|
||||||
|
);
|
||||||
|
|
||||||
|
context.drawImage(texture, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw texture pattern to a temporary canvas
|
||||||
|
*/
|
||||||
|
drawTexture(type, color1, color2, scale, width, height) {
|
||||||
|
const tempCanvas = document.createElement('canvas');
|
||||||
|
tempCanvas.width = width;
|
||||||
|
tempCanvas.height = height;
|
||||||
|
const ctx = tempCanvas.getContext('2d');
|
||||||
|
|
||||||
|
// Fill base color
|
||||||
|
ctx.fillStyle = color1;
|
||||||
|
ctx.fillRect(0, 0, width, height);
|
||||||
|
|
||||||
|
// Draw pattern
|
||||||
|
ctx.fillStyle = color2;
|
||||||
|
const size = Math.max(2, Math.floor(scale / 10));
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'dots':
|
||||||
|
this.drawDots(ctx, width, height, size);
|
||||||
|
break;
|
||||||
|
case 'grid':
|
||||||
|
this.drawGrid(ctx, width, height, size);
|
||||||
|
break;
|
||||||
|
case 'diagonal':
|
||||||
|
this.drawDiagonal(ctx, width, height, size);
|
||||||
|
break;
|
||||||
|
case 'checkerboard':
|
||||||
|
this.drawCheckerboard(ctx, width, height, size);
|
||||||
|
break;
|
||||||
|
case 'noise':
|
||||||
|
this.drawNoise(ctx, width, height);
|
||||||
|
break;
|
||||||
|
case 'stars':
|
||||||
|
this.drawStars(ctx, width, height, scale);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return tempCanvas;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw dots pattern
|
||||||
|
*/
|
||||||
|
drawDots(ctx, width, height, size) {
|
||||||
|
for (let y = 0; y < height; y += size * 2) {
|
||||||
|
for (let x = 0; x < width; x += size * 2) {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(x, y, size / 2, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw grid pattern
|
||||||
|
*/
|
||||||
|
drawGrid(ctx, width, height, size) {
|
||||||
|
// Vertical lines
|
||||||
|
for (let x = 0; x < width; x += size) {
|
||||||
|
ctx.fillRect(x, 0, 1, height);
|
||||||
|
}
|
||||||
|
// Horizontal lines
|
||||||
|
for (let y = 0; y < height; y += size) {
|
||||||
|
ctx.fillRect(0, y, width, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw diagonal lines pattern
|
||||||
|
*/
|
||||||
|
drawDiagonal(ctx, width, height, size) {
|
||||||
|
for (let i = -height; i < width; i += size) {
|
||||||
|
ctx.fillRect(i, 0, 2, height);
|
||||||
|
ctx.save();
|
||||||
|
ctx.translate(i + 1, 0);
|
||||||
|
ctx.rotate(Math.PI / 4);
|
||||||
|
ctx.fillRect(0, 0, 2, Math.max(width, height));
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw checkerboard pattern
|
||||||
|
*/
|
||||||
|
drawCheckerboard(ctx, width, height, size) {
|
||||||
|
for (let y = 0; y < height; y += size) {
|
||||||
|
for (let x = 0; x < width; x += size) {
|
||||||
|
if ((x / size + y / size) % 2 === 0) {
|
||||||
|
ctx.fillRect(x, y, size, size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw random noise pattern
|
||||||
|
*/
|
||||||
|
drawNoise(ctx, width, height) {
|
||||||
|
for (let y = 0; y < height; y++) {
|
||||||
|
for (let x = 0; x < width; x++) {
|
||||||
|
if (Math.random() > 0.5) {
|
||||||
|
ctx.fillRect(x, y, 1, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw stars pattern
|
||||||
|
*/
|
||||||
|
drawStars(ctx, width, height, scale) {
|
||||||
|
const numStars = scale;
|
||||||
|
for (let i = 0; i < numStars; i++) {
|
||||||
|
const x = Math.floor(Math.random() * width);
|
||||||
|
const y = Math.floor(Math.random() * height);
|
||||||
|
|
||||||
|
// Draw plus-shape star
|
||||||
|
ctx.fillRect(x, y, 1, 1); // Center
|
||||||
|
ctx.fillRect(x - 1, y, 1, 1); // Left
|
||||||
|
ctx.fillRect(x + 1, y, 1, 1); // Right
|
||||||
|
ctx.fillRect(x, y - 1, 1, 1); // Top
|
||||||
|
ctx.fillRect(x, y + 1, 1, 1); // Bottom
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-register effect
|
||||||
|
export function register(generator) {
|
||||||
|
generator.registerEffect(new TextureBackgroundEffect());
|
||||||
|
}
|
||||||
401
static/js/button-generator/effects/border.js
Normal file
401
static/js/button-generator/effects/border.js
Normal file
|
|
@ -0,0 +1,401 @@
|
||||||
|
import { ButtonEffect } from "../effect-base.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Border effect
|
||||||
|
* Draws borders around the button with various styles
|
||||||
|
*/
|
||||||
|
export class BorderEffect extends ButtonEffect {
|
||||||
|
constructor() {
|
||||||
|
super({
|
||||||
|
id: "border",
|
||||||
|
name: "Border",
|
||||||
|
type: "border",
|
||||||
|
category: "Border",
|
||||||
|
renderOrder: 10,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
defineControls() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: "border-width",
|
||||||
|
type: "range",
|
||||||
|
label: "Border Width",
|
||||||
|
defaultValue: 2,
|
||||||
|
min: 0,
|
||||||
|
max: 5,
|
||||||
|
step: 1,
|
||||||
|
description: "Width of border in pixels",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "border-color",
|
||||||
|
type: "color",
|
||||||
|
label: "Border Color",
|
||||||
|
defaultValue: "#000000",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "border-style",
|
||||||
|
type: "select",
|
||||||
|
label: "Border Style",
|
||||||
|
defaultValue: "solid",
|
||||||
|
options: [
|
||||||
|
{ value: "solid", label: "Solid" },
|
||||||
|
{ value: "dashed", label: "Dashed" },
|
||||||
|
{ value: "dotted", label: "Dotted" },
|
||||||
|
{ 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) {
|
||||||
|
const width = controlValues["border-width"] || 0;
|
||||||
|
return width > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
apply(context, controlValues, animState, renderData) {
|
||||||
|
const width = controlValues["border-width"] || 0;
|
||||||
|
if (width === 0) return;
|
||||||
|
|
||||||
|
const color = controlValues["border-color"] || "#000000";
|
||||||
|
const style = controlValues["border-style"] || "solid";
|
||||||
|
|
||||||
|
if (style === "solid") {
|
||||||
|
this.drawSolidBorder(context, width, color, renderData);
|
||||||
|
} else if (style === "dashed") {
|
||||||
|
this.drawDashedBorder(context, width, color, renderData);
|
||||||
|
} 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);
|
||||||
|
} 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw solid border
|
||||||
|
*/
|
||||||
|
drawSolidBorder(context, width, color, renderData) {
|
||||||
|
context.strokeStyle = color;
|
||||||
|
context.lineWidth = width;
|
||||||
|
context.strokeRect(
|
||||||
|
width / 2,
|
||||||
|
width / 2,
|
||||||
|
renderData.width - width,
|
||||||
|
renderData.height - width,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw 3D inset/outset border
|
||||||
|
*/
|
||||||
|
draw3DBorder(context, width, color, isOutset, renderData) {
|
||||||
|
const w = renderData.width;
|
||||||
|
const h = renderData.height;
|
||||||
|
const t = width;
|
||||||
|
|
||||||
|
const normalized = color.toLowerCase();
|
||||||
|
const isPureBlack = normalized === "#000000";
|
||||||
|
const isPureWhite = normalized === "#ffffff";
|
||||||
|
|
||||||
|
let lightColor;
|
||||||
|
let darkColor;
|
||||||
|
|
||||||
|
if (isPureBlack || isPureWhite) {
|
||||||
|
lightColor = isOutset ? "#ffffff" : "#000000";
|
||||||
|
darkColor = isOutset ? "#000000" : "#ffffff";
|
||||||
|
} 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw ridge border (double 3D effect)
|
||||||
|
*/
|
||||||
|
drawRidgeBorder(context, width, renderData) {
|
||||||
|
// Outer ridge (light)
|
||||||
|
context.strokeStyle = "#ffffff";
|
||||||
|
context.lineWidth = width / 2;
|
||||||
|
context.strokeRect(
|
||||||
|
width / 4,
|
||||||
|
width / 4,
|
||||||
|
renderData.width - width / 2,
|
||||||
|
renderData.height - width / 2,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Inner ridge (dark)
|
||||||
|
context.strokeStyle = "#000000";
|
||||||
|
context.strokeRect(
|
||||||
|
(width * 3) / 4,
|
||||||
|
(width * 3) / 4,
|
||||||
|
renderData.width - 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
|
||||||
|
export function register(generator) {
|
||||||
|
generator.registerEffect(new BorderEffect());
|
||||||
|
}
|
||||||
93
static/js/button-generator/effects/glitch.js
Normal file
93
static/js/button-generator/effects/glitch.js
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
import { ButtonEffect } from '../effect-base.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Glitch effect
|
||||||
|
* Creates horizontal scanline displacement for a glitchy look
|
||||||
|
*/
|
||||||
|
export class GlitchEffect extends ButtonEffect {
|
||||||
|
constructor() {
|
||||||
|
super({
|
||||||
|
id: 'glitch',
|
||||||
|
name: 'Glitch',
|
||||||
|
type: 'general',
|
||||||
|
category: 'Visual Effects',
|
||||||
|
renderOrder: 80
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
defineControls() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: 'animate-glitch',
|
||||||
|
type: 'checkbox',
|
||||||
|
label: 'Glitch Effect',
|
||||||
|
defaultValue: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'glitch-intensity',
|
||||||
|
type: 'range',
|
||||||
|
label: 'Glitch Intensity',
|
||||||
|
defaultValue: 5,
|
||||||
|
min: 1,
|
||||||
|
max: 20,
|
||||||
|
step: 1,
|
||||||
|
showWhen: 'animate-glitch',
|
||||||
|
description: 'Maximum pixel offset for glitch'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
isEnabled(controlValues) {
|
||||||
|
return controlValues['animate-glitch'] === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
apply(context, controlValues, animState, renderData) {
|
||||||
|
if (!animState) return;
|
||||||
|
|
||||||
|
const intensity = controlValues['glitch-intensity'] || 5;
|
||||||
|
const imageData = context.getImageData(0, 0, renderData.width, renderData.height);
|
||||||
|
|
||||||
|
// Randomly glitch ~10% of scanlines per frame
|
||||||
|
const glitchProbability = 0.1;
|
||||||
|
const maxOffset = intensity;
|
||||||
|
|
||||||
|
for (let y = 0; y < renderData.height; y++) {
|
||||||
|
if (Math.random() < glitchProbability) {
|
||||||
|
const offset = Math.floor((Math.random() - 0.5) * maxOffset * 2);
|
||||||
|
this.shiftScanline(imageData, y, offset, renderData.width);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
context.putImageData(imageData, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shift a horizontal scanline by offset pixels (with wrapping)
|
||||||
|
*/
|
||||||
|
shiftScanline(imageData, y, offset, width) {
|
||||||
|
const rowStart = y * width * 4;
|
||||||
|
const rowData = new Uint8ClampedArray(width * 4);
|
||||||
|
|
||||||
|
// Copy row
|
||||||
|
for (let i = 0; i < width * 4; i++) {
|
||||||
|
rowData[i] = imageData.data[rowStart + i];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shift and wrap
|
||||||
|
for (let x = 0; x < width; x++) {
|
||||||
|
let sourceX = (x - offset + width) % width;
|
||||||
|
let destIdx = rowStart + x * 4;
|
||||||
|
let srcIdx = sourceX * 4;
|
||||||
|
|
||||||
|
imageData.data[destIdx] = rowData[srcIdx];
|
||||||
|
imageData.data[destIdx + 1] = rowData[srcIdx + 1];
|
||||||
|
imageData.data[destIdx + 2] = rowData[srcIdx + 2];
|
||||||
|
imageData.data[destIdx + 3] = rowData[srcIdx + 3];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-register effect
|
||||||
|
export function register(generator) {
|
||||||
|
generator.registerEffect(new GlitchEffect());
|
||||||
|
}
|
||||||
170
static/js/button-generator/effects/hologram.js
Normal file
170
static/js/button-generator/effects/hologram.js
Normal file
|
|
@ -0,0 +1,170 @@
|
||||||
|
import { ButtonEffect } from '../effect-base.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hologram effect
|
||||||
|
* Creates a futuristic holographic appearance with glitches and scan lines
|
||||||
|
*/
|
||||||
|
export class HologramEffect extends ButtonEffect {
|
||||||
|
constructor() {
|
||||||
|
super({
|
||||||
|
id: 'hologram',
|
||||||
|
name: 'Hologram',
|
||||||
|
type: 'general',
|
||||||
|
category: 'Visual Effects',
|
||||||
|
renderOrder: 88 // Near the end, after most other effects
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
defineControls() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: 'animate-hologram',
|
||||||
|
type: 'checkbox',
|
||||||
|
label: 'Hologram Effect',
|
||||||
|
defaultValue: false,
|
||||||
|
description: 'Futuristic holographic appearance'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'hologram-intensity',
|
||||||
|
type: 'range',
|
||||||
|
label: 'Effect Intensity',
|
||||||
|
defaultValue: 50,
|
||||||
|
min: 10,
|
||||||
|
max: 100,
|
||||||
|
step: 5,
|
||||||
|
showWhen: 'animate-hologram',
|
||||||
|
description: 'Strength of hologram effect'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'hologram-glitch-freq',
|
||||||
|
type: 'range',
|
||||||
|
label: 'Glitch Frequency',
|
||||||
|
defaultValue: 30,
|
||||||
|
min: 0,
|
||||||
|
max: 100,
|
||||||
|
step: 10,
|
||||||
|
showWhen: 'animate-hologram',
|
||||||
|
description: 'How often glitches occur'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'hologram-color',
|
||||||
|
type: 'color',
|
||||||
|
label: 'Hologram Tint',
|
||||||
|
defaultValue: '#00ffff',
|
||||||
|
showWhen: 'animate-hologram',
|
||||||
|
description: 'Color tint for hologram effect'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
isEnabled(controlValues) {
|
||||||
|
return controlValues['animate-hologram'] === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
apply(context, controlValues, animState, renderData) {
|
||||||
|
if (!animState) return;
|
||||||
|
|
||||||
|
const intensity = (controlValues['hologram-intensity'] || 50) / 100;
|
||||||
|
const glitchFreq = (controlValues['hologram-glitch-freq'] || 30) / 100;
|
||||||
|
const color = controlValues['hologram-color'] || '#00ffff';
|
||||||
|
|
||||||
|
// Get current canvas content
|
||||||
|
const imageData = context.getImageData(0, 0, renderData.width, renderData.height);
|
||||||
|
const data = imageData.data;
|
||||||
|
|
||||||
|
// Parse hologram color for tinting
|
||||||
|
const hexColor = color.replace('#', '');
|
||||||
|
const r = parseInt(hexColor.substr(0, 2), 16);
|
||||||
|
const g = parseInt(hexColor.substr(2, 2), 16);
|
||||||
|
const b = parseInt(hexColor.substr(4, 2), 16);
|
||||||
|
|
||||||
|
// Apply holographic tint
|
||||||
|
for (let i = 0; i < data.length; i += 4) {
|
||||||
|
// Mix with hologram color
|
||||||
|
data[i] = data[i] * (1 - intensity * 0.3) + r * intensity * 0.3; // Red
|
||||||
|
data[i + 1] = data[i + 1] * (1 - intensity * 0.5) + g * intensity * 0.5; // Green (more cyan)
|
||||||
|
data[i + 2] = data[i + 2] * (1 - intensity * 0.5) + b * intensity * 0.5; // Blue (more cyan)
|
||||||
|
}
|
||||||
|
|
||||||
|
context.putImageData(imageData, 0, 0);
|
||||||
|
|
||||||
|
// Add horizontal scan lines
|
||||||
|
context.globalAlpha = 0.05 * intensity;
|
||||||
|
context.fillStyle = '#000000';
|
||||||
|
for (let y = 0; y < renderData.height; y += 2) {
|
||||||
|
context.fillRect(0, y, renderData.width, 1);
|
||||||
|
}
|
||||||
|
context.globalAlpha = 1.0;
|
||||||
|
|
||||||
|
// Add moving highlight scan line
|
||||||
|
const scanY = (animState.progress * renderData.height) % renderData.height;
|
||||||
|
const gradient = context.createLinearGradient(0, scanY - 3, 0, scanY + 3);
|
||||||
|
gradient.addColorStop(0, 'rgba(0, 255, 255, 0)');
|
||||||
|
gradient.addColorStop(0.5, `rgba(${r}, ${g}, ${b}, ${0.3 * intensity})`);
|
||||||
|
gradient.addColorStop(1, 'rgba(0, 255, 255, 0)');
|
||||||
|
context.fillStyle = gradient;
|
||||||
|
context.fillRect(0, scanY - 3, renderData.width, 6);
|
||||||
|
|
||||||
|
// Random glitches
|
||||||
|
if (Math.random() < glitchFreq * 0.1) {
|
||||||
|
const glitchY = Math.floor(Math.random() * renderData.height);
|
||||||
|
const glitchHeight = Math.floor(2 + Math.random() * 4);
|
||||||
|
const offset = (Math.random() - 0.5) * 6 * intensity;
|
||||||
|
|
||||||
|
const sliceData = context.getImageData(0, glitchY, renderData.width, glitchHeight);
|
||||||
|
context.putImageData(sliceData, offset, glitchY);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add chromatic aberration on edges
|
||||||
|
if (intensity > 0.3) {
|
||||||
|
const originalImage = context.getImageData(0, 0, renderData.width, renderData.height);
|
||||||
|
const aberration = 2 * intensity;
|
||||||
|
|
||||||
|
// Slight red shift right
|
||||||
|
const redShift = context.getImageData(0, 0, renderData.width, renderData.height);
|
||||||
|
for (let i = 0; i < redShift.data.length; i += 4) {
|
||||||
|
const pixelIndex = i / 4;
|
||||||
|
const x = pixelIndex % renderData.width;
|
||||||
|
if (x < 3 || x > renderData.width - 3) {
|
||||||
|
const sourceIndex = ((pixelIndex + Math.floor(aberration)) * 4);
|
||||||
|
if (sourceIndex < originalImage.data.length) {
|
||||||
|
redShift.data[i] = originalImage.data[sourceIndex];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Slight blue shift left
|
||||||
|
const blueShift = context.getImageData(0, 0, renderData.width, renderData.height);
|
||||||
|
for (let i = 0; i < blueShift.data.length; i += 4) {
|
||||||
|
const pixelIndex = i / 4;
|
||||||
|
const x = pixelIndex % renderData.width;
|
||||||
|
if (x < 3 || x > renderData.width - 3) {
|
||||||
|
const sourceIndex = ((pixelIndex - Math.floor(aberration)) * 4);
|
||||||
|
if (sourceIndex >= 0 && sourceIndex < originalImage.data.length) {
|
||||||
|
blueShift.data[i + 2] = originalImage.data[sourceIndex + 2];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
context.putImageData(redShift, 0, 0);
|
||||||
|
context.globalCompositeOperation = 'screen';
|
||||||
|
context.globalAlpha = 0.3;
|
||||||
|
context.putImageData(blueShift, 0, 0);
|
||||||
|
context.globalCompositeOperation = 'source-over';
|
||||||
|
context.globalAlpha = 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add flickering effect
|
||||||
|
if (Math.random() < 0.05) {
|
||||||
|
context.globalAlpha = 0.9 + Math.random() * 0.1;
|
||||||
|
context.fillStyle = 'rgba(255, 255, 255, 0.05)';
|
||||||
|
context.fillRect(0, 0, renderData.width, renderData.height);
|
||||||
|
context.globalAlpha = 1.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-register effect
|
||||||
|
export function register(generator) {
|
||||||
|
generator.registerEffect(new HologramEffect());
|
||||||
|
}
|
||||||
68
static/js/button-generator/effects/noise.js
Normal file
68
static/js/button-generator/effects/noise.js
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
import { ButtonEffect } from '../effect-base.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Noise/Static effect
|
||||||
|
* Adds random pixel noise for a static/interference look
|
||||||
|
*/
|
||||||
|
export class NoiseEffect extends ButtonEffect {
|
||||||
|
constructor() {
|
||||||
|
super({
|
||||||
|
id: 'noise',
|
||||||
|
name: 'Noise',
|
||||||
|
type: 'general',
|
||||||
|
category: 'Visual Effects',
|
||||||
|
renderOrder: 90
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
defineControls() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: 'animate-noise',
|
||||||
|
type: 'checkbox',
|
||||||
|
label: 'Noise Effect',
|
||||||
|
defaultValue: false,
|
||||||
|
description: 'Random static/interference'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'noise-intensity',
|
||||||
|
type: 'range',
|
||||||
|
label: 'Noise Intensity',
|
||||||
|
defaultValue: 0.1,
|
||||||
|
min: 0.01,
|
||||||
|
max: 0.5,
|
||||||
|
step: 0.01,
|
||||||
|
showWhen: 'animate-noise',
|
||||||
|
description: 'Amount of noise'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
isEnabled(controlValues) {
|
||||||
|
return controlValues['animate-noise'] === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
apply(context, controlValues, animState, renderData) {
|
||||||
|
if (!animState) return;
|
||||||
|
|
||||||
|
const intensity = controlValues['noise-intensity'] || 0.1;
|
||||||
|
const imageData = context.getImageData(0, 0, renderData.width, renderData.height);
|
||||||
|
|
||||||
|
for (let i = 0; i < imageData.data.length; i += 4) {
|
||||||
|
// Random noise value
|
||||||
|
const noise = (Math.random() - 0.5) * 255 * intensity;
|
||||||
|
|
||||||
|
imageData.data[i] = Math.max(0, Math.min(255, imageData.data[i] + noise));
|
||||||
|
imageData.data[i + 1] = Math.max(0, Math.min(255, imageData.data[i + 1] + noise));
|
||||||
|
imageData.data[i + 2] = Math.max(0, Math.min(255, imageData.data[i + 2] + noise));
|
||||||
|
// Alpha unchanged
|
||||||
|
}
|
||||||
|
|
||||||
|
context.putImageData(imageData, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-register effect
|
||||||
|
export function register(generator) {
|
||||||
|
generator.registerEffect(new NoiseEffect());
|
||||||
|
}
|
||||||
62
static/js/button-generator/effects/pulse.js
Normal file
62
static/js/button-generator/effects/pulse.js
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
import { ButtonEffect } from '../effect-base.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pulse effect
|
||||||
|
* Scales the button content up and down
|
||||||
|
*/
|
||||||
|
export class PulseEffect extends ButtonEffect {
|
||||||
|
constructor() {
|
||||||
|
super({
|
||||||
|
id: 'pulse',
|
||||||
|
name: 'Pulse',
|
||||||
|
type: 'transform',
|
||||||
|
category: 'Visual Effects',
|
||||||
|
renderOrder: 1 // Must run before any drawing
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
defineControls() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: 'animate-pulse',
|
||||||
|
type: 'checkbox',
|
||||||
|
label: 'Pulse Effect',
|
||||||
|
defaultValue: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'pulse-scale',
|
||||||
|
type: 'range',
|
||||||
|
label: 'Pulse Scale',
|
||||||
|
defaultValue: 1.2,
|
||||||
|
min: 1.0,
|
||||||
|
max: 2.0,
|
||||||
|
step: 0.05,
|
||||||
|
showWhen: 'animate-pulse',
|
||||||
|
description: 'Maximum scale size'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
isEnabled(controlValues) {
|
||||||
|
return controlValues['animate-pulse'] === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
apply(context, controlValues, animState, renderData) {
|
||||||
|
if (!animState) return;
|
||||||
|
|
||||||
|
const maxScale = controlValues['pulse-scale'] || 1.2;
|
||||||
|
const minScale = 1.0;
|
||||||
|
const scale = minScale + (maxScale - minScale) *
|
||||||
|
(Math.sin(animState.getPhase(1.0)) * 0.5 + 0.5);
|
||||||
|
|
||||||
|
// Apply transformation (context save/restore handled by caller)
|
||||||
|
context.translate(renderData.centerX, renderData.centerY);
|
||||||
|
context.scale(scale, scale);
|
||||||
|
context.translate(-renderData.centerX, -renderData.centerY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-register effect
|
||||||
|
export function register(generator) {
|
||||||
|
generator.registerEffect(new PulseEffect());
|
||||||
|
}
|
||||||
100
static/js/button-generator/effects/rainbow-text.js
Normal file
100
static/js/button-generator/effects/rainbow-text.js
Normal file
|
|
@ -0,0 +1,100 @@
|
||||||
|
import { ButtonEffect } from '../effect-base.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rainbow text animation effect
|
||||||
|
* Cycles text color through rainbow hues
|
||||||
|
*/
|
||||||
|
export class RainbowTextEffect extends ButtonEffect {
|
||||||
|
constructor(textLineNumber = 1) {
|
||||||
|
const suffix = textLineNumber === 1 ? '' : '2';
|
||||||
|
super({
|
||||||
|
id: `text-rainbow${suffix}`,
|
||||||
|
name: `Rainbow Text ${textLineNumber}`,
|
||||||
|
type: textLineNumber === 1 ? 'text' : 'text2',
|
||||||
|
category: textLineNumber === 1 ? 'Text Line 1' : 'Text Line 2',
|
||||||
|
renderOrder: 5, // Apply before wave (lower order)
|
||||||
|
textLineNumber: textLineNumber // Pass through config
|
||||||
|
});
|
||||||
|
this.textLineNumber = textLineNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
defineControls() {
|
||||||
|
const textLineNumber = this.textLineNumber || this.config?.textLineNumber || 1;
|
||||||
|
const suffix = textLineNumber === 1 ? '' : '2';
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: `animate-text-rainbow${suffix}`,
|
||||||
|
type: 'checkbox',
|
||||||
|
label: 'Rainbow Animation',
|
||||||
|
defaultValue: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: `text-rainbow-speed${suffix}`,
|
||||||
|
type: 'range',
|
||||||
|
label: 'Rainbow Speed',
|
||||||
|
defaultValue: 1,
|
||||||
|
min: 0.1,
|
||||||
|
max: 5,
|
||||||
|
step: 0.1,
|
||||||
|
showWhen: `animate-text-rainbow${suffix}`,
|
||||||
|
description: 'Speed of color cycling'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
isEnabled(controlValues) {
|
||||||
|
const suffix = this.textLineNumber === 1 ? '' : '2';
|
||||||
|
return controlValues[`animate-text-rainbow${suffix}`] === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
apply(context, controlValues, animState, renderData) {
|
||||||
|
if (!animState) return; // Rainbow requires animation
|
||||||
|
|
||||||
|
const suffix = this.textLineNumber === 1 ? '' : '2';
|
||||||
|
|
||||||
|
// Get text configuration
|
||||||
|
const text = controlValues[`button-text${suffix}`] || '';
|
||||||
|
const enabled = controlValues[`text${suffix}-enabled`];
|
||||||
|
if (!text || !enabled) return;
|
||||||
|
|
||||||
|
// Check if wave is also enabled - if so, skip (wave will handle rainbow)
|
||||||
|
if (controlValues[`animate-text-wave${suffix}`]) return;
|
||||||
|
|
||||||
|
const fontSize = controlValues[`font-size${suffix}`] || 12;
|
||||||
|
const fontWeight = controlValues[`font-bold${suffix}`] ? 'bold' : 'normal';
|
||||||
|
const fontStyle = controlValues[`font-italic${suffix}`] ? 'italic' : 'normal';
|
||||||
|
const fontFamily = controlValues[`font-family${suffix}`] || 'Arial';
|
||||||
|
|
||||||
|
const x = (controlValues[`text${suffix}-x`] / 100) * renderData.width;
|
||||||
|
const y = (controlValues[`text${suffix}-y`] / 100) * renderData.height;
|
||||||
|
|
||||||
|
const speed = controlValues[`text-rainbow-speed${suffix}`] || 1;
|
||||||
|
|
||||||
|
// Calculate rainbow color
|
||||||
|
const hue = (animState.progress * speed * 360) % 360;
|
||||||
|
const fillStyle = `hsl(${hue}, 80%, 60%)`;
|
||||||
|
const strokeStyle = `hsl(${hue}, 80%, 30%)`;
|
||||||
|
|
||||||
|
// Set font
|
||||||
|
context.font = `${fontStyle} ${fontWeight} ${fontSize}px "${fontFamily}"`;
|
||||||
|
context.textAlign = 'center';
|
||||||
|
context.textBaseline = 'middle';
|
||||||
|
|
||||||
|
// Draw outline if enabled
|
||||||
|
if (controlValues[`text${suffix}-outline`]) {
|
||||||
|
context.strokeStyle = strokeStyle;
|
||||||
|
context.lineWidth = 2;
|
||||||
|
context.strokeText(text, x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw text
|
||||||
|
context.fillStyle = fillStyle;
|
||||||
|
context.fillText(text, x, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-register effect
|
||||||
|
export function register(generator) {
|
||||||
|
generator.registerEffect(new RainbowTextEffect(1));
|
||||||
|
generator.registerEffect(new RainbowTextEffect(2));
|
||||||
|
}
|
||||||
85
static/js/button-generator/effects/rgb-split.js
Normal file
85
static/js/button-generator/effects/rgb-split.js
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
import { ButtonEffect } from '../effect-base.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RGB Split / Chromatic Aberration effect
|
||||||
|
* Separates color channels for a glitchy chromatic aberration look
|
||||||
|
*/
|
||||||
|
export class RgbSplitEffect extends ButtonEffect {
|
||||||
|
constructor() {
|
||||||
|
super({
|
||||||
|
id: 'rgb-split',
|
||||||
|
name: 'RGB Split',
|
||||||
|
type: 'general',
|
||||||
|
category: 'Visual Effects',
|
||||||
|
renderOrder: 85
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
defineControls() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: 'animate-rgb-split',
|
||||||
|
type: 'checkbox',
|
||||||
|
label: 'RGB Split',
|
||||||
|
defaultValue: false,
|
||||||
|
description: 'Chromatic aberration effect'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'rgb-split-intensity',
|
||||||
|
type: 'range',
|
||||||
|
label: 'Split Intensity',
|
||||||
|
defaultValue: 2,
|
||||||
|
min: 1,
|
||||||
|
max: 10,
|
||||||
|
step: 0.5,
|
||||||
|
showWhen: 'animate-rgb-split',
|
||||||
|
description: 'Pixel offset for color channels'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
isEnabled(controlValues) {
|
||||||
|
return controlValues['animate-rgb-split'] === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
apply(context, controlValues, animState, renderData) {
|
||||||
|
if (!animState) return;
|
||||||
|
|
||||||
|
const intensity = controlValues['rgb-split-intensity'] || 2;
|
||||||
|
const imageData = context.getImageData(0, 0, renderData.width, renderData.height);
|
||||||
|
const result = context.createImageData(renderData.width, renderData.height);
|
||||||
|
|
||||||
|
// Oscillating offset
|
||||||
|
const phase = Math.sin(animState.getPhase(1.0));
|
||||||
|
const offsetX = Math.round(phase * intensity);
|
||||||
|
|
||||||
|
for (let y = 0; y < renderData.height; y++) {
|
||||||
|
for (let x = 0; x < renderData.width; x++) {
|
||||||
|
const idx = (y * renderData.width + x) * 4;
|
||||||
|
|
||||||
|
// Red channel - shift left
|
||||||
|
const redX = Math.max(0, Math.min(renderData.width - 1, x - offsetX));
|
||||||
|
const redIdx = (y * renderData.width + redX) * 4;
|
||||||
|
result.data[idx] = imageData.data[redIdx];
|
||||||
|
|
||||||
|
// Green channel - no shift
|
||||||
|
result.data[idx + 1] = imageData.data[idx + 1];
|
||||||
|
|
||||||
|
// Blue channel - shift right
|
||||||
|
const blueX = Math.max(0, Math.min(renderData.width - 1, x + offsetX));
|
||||||
|
const blueIdx = (y * renderData.width + blueX) * 4;
|
||||||
|
result.data[idx + 2] = imageData.data[blueIdx + 2];
|
||||||
|
|
||||||
|
// Alpha channel
|
||||||
|
result.data[idx + 3] = imageData.data[idx + 3];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
context.putImageData(result, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-register effect
|
||||||
|
export function register(generator) {
|
||||||
|
generator.registerEffect(new RgbSplitEffect());
|
||||||
|
}
|
||||||
72
static/js/button-generator/effects/rotate.js
Normal file
72
static/js/button-generator/effects/rotate.js
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
import { ButtonEffect } from '../effect-base.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rotate effect
|
||||||
|
* Rotates the button back and forth
|
||||||
|
*/
|
||||||
|
export class RotateEffect extends ButtonEffect {
|
||||||
|
constructor() {
|
||||||
|
super({
|
||||||
|
id: 'rotate',
|
||||||
|
name: 'Rotate',
|
||||||
|
type: 'transform',
|
||||||
|
category: 'Visual Effects',
|
||||||
|
renderOrder: 2 // Must run before any drawing (after pulse)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
defineControls() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: 'animate-rotate',
|
||||||
|
type: 'checkbox',
|
||||||
|
label: 'Rotate Effect',
|
||||||
|
defaultValue: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'rotate-angle',
|
||||||
|
type: 'range',
|
||||||
|
label: 'Max Angle',
|
||||||
|
defaultValue: 15,
|
||||||
|
min: 1,
|
||||||
|
max: 45,
|
||||||
|
step: 1,
|
||||||
|
showWhen: 'animate-rotate',
|
||||||
|
description: 'Maximum rotation angle in degrees'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'rotate-speed',
|
||||||
|
type: 'range',
|
||||||
|
label: 'Rotation Speed',
|
||||||
|
defaultValue: 1,
|
||||||
|
min: 0.1,
|
||||||
|
max: 3,
|
||||||
|
step: 0.1,
|
||||||
|
showWhen: 'animate-rotate',
|
||||||
|
description: 'Speed of rotation'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
isEnabled(controlValues) {
|
||||||
|
return controlValues['animate-rotate'] === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
apply(context, controlValues, animState, renderData) {
|
||||||
|
if (!animState) return;
|
||||||
|
|
||||||
|
const maxAngle = controlValues['rotate-angle'] || 15;
|
||||||
|
const speed = controlValues['rotate-speed'] || 1;
|
||||||
|
const angle = Math.sin(animState.getPhase(speed)) * maxAngle * (Math.PI / 180);
|
||||||
|
|
||||||
|
// Apply transformation (context save/restore handled by caller)
|
||||||
|
context.translate(renderData.centerX, renderData.centerY);
|
||||||
|
context.rotate(angle);
|
||||||
|
context.translate(-renderData.centerX, -renderData.centerY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-register effect
|
||||||
|
export function register(generator) {
|
||||||
|
generator.registerEffect(new RotateEffect());
|
||||||
|
}
|
||||||
79
static/js/button-generator/effects/scanline.js
Normal file
79
static/js/button-generator/effects/scanline.js
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
import { ButtonEffect } from '../effect-base.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scanline effect
|
||||||
|
* Creates CRT-style horizontal lines
|
||||||
|
*/
|
||||||
|
export class ScanlineEffect extends ButtonEffect {
|
||||||
|
constructor() {
|
||||||
|
super({
|
||||||
|
id: 'scanline',
|
||||||
|
name: 'Scanline',
|
||||||
|
type: 'general',
|
||||||
|
category: 'Visual Effects',
|
||||||
|
renderOrder: 75
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
defineControls() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: 'animate-scanline',
|
||||||
|
type: 'checkbox',
|
||||||
|
label: 'Scanline Effect',
|
||||||
|
defaultValue: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'scanline-intensity',
|
||||||
|
type: 'range',
|
||||||
|
label: 'Scanline Intensity',
|
||||||
|
defaultValue: 0.3,
|
||||||
|
min: 0.1,
|
||||||
|
max: 0.8,
|
||||||
|
step: 0.05,
|
||||||
|
showWhen: 'animate-scanline',
|
||||||
|
description: 'Darkness of scanlines'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'scanline-speed',
|
||||||
|
type: 'range',
|
||||||
|
label: 'Scanline Speed',
|
||||||
|
defaultValue: 1,
|
||||||
|
min: 0.1,
|
||||||
|
max: 3,
|
||||||
|
step: 0.1,
|
||||||
|
showWhen: 'animate-scanline',
|
||||||
|
description: 'Movement speed of scanlines'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
isEnabled(controlValues) {
|
||||||
|
return controlValues['animate-scanline'] === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
apply(context, controlValues, animState, renderData) {
|
||||||
|
if (!animState) return;
|
||||||
|
|
||||||
|
const intensity = controlValues['scanline-intensity'] || 0.3;
|
||||||
|
const speed = controlValues['scanline-speed'] || 1;
|
||||||
|
|
||||||
|
// Create overlay with scanlines
|
||||||
|
context.globalCompositeOperation = 'multiply';
|
||||||
|
context.fillStyle = `rgba(0, 0, 0, ${intensity})`;
|
||||||
|
|
||||||
|
// Animate scanline position
|
||||||
|
const offset = (animState.progress * speed * renderData.height) % 2;
|
||||||
|
|
||||||
|
for (let y = offset; y < renderData.height; y += 2) {
|
||||||
|
context.fillRect(0, Math.floor(y), renderData.width, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
context.globalCompositeOperation = 'source-over';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-register effect
|
||||||
|
export function register(generator) {
|
||||||
|
generator.registerEffect(new ScanlineEffect());
|
||||||
|
}
|
||||||
57
static/js/button-generator/effects/shimmer.js
Normal file
57
static/js/button-generator/effects/shimmer.js
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
import { ButtonEffect } from '../effect-base.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shimmer effect
|
||||||
|
* Creates a sweeping light/shine effect across the button
|
||||||
|
*/
|
||||||
|
export class ShimmerEffect extends ButtonEffect {
|
||||||
|
constructor() {
|
||||||
|
super({
|
||||||
|
id: 'shimmer',
|
||||||
|
name: 'Shimmer',
|
||||||
|
type: 'general',
|
||||||
|
category: 'Visual Effects',
|
||||||
|
renderOrder: 70
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
defineControls() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: 'animate-shimmer',
|
||||||
|
type: 'checkbox',
|
||||||
|
label: 'Shimmer Effect',
|
||||||
|
defaultValue: false,
|
||||||
|
description: 'Sweeping light effect'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
isEnabled(controlValues) {
|
||||||
|
return controlValues['animate-shimmer'] === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
apply(context, controlValues, animState, renderData) {
|
||||||
|
if (!animState) return;
|
||||||
|
|
||||||
|
const shimmerX = animState.progress * (renderData.width + 40) - 20;
|
||||||
|
|
||||||
|
const gradient = context.createLinearGradient(
|
||||||
|
shimmerX - 15,
|
||||||
|
0,
|
||||||
|
shimmerX + 15,
|
||||||
|
renderData.height
|
||||||
|
);
|
||||||
|
gradient.addColorStop(0, 'rgba(255, 255, 255, 0)');
|
||||||
|
gradient.addColorStop(0.5, 'rgba(255, 255, 255, 0.3)');
|
||||||
|
gradient.addColorStop(1, 'rgba(255, 255, 255, 0)');
|
||||||
|
|
||||||
|
context.fillStyle = gradient;
|
||||||
|
context.fillRect(0, 0, renderData.width, renderData.height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-register effect
|
||||||
|
export function register(generator) {
|
||||||
|
generator.registerEffect(new ShimmerEffect());
|
||||||
|
}
|
||||||
162
static/js/button-generator/effects/spin-text.js
Normal file
162
static/js/button-generator/effects/spin-text.js
Normal file
|
|
@ -0,0 +1,162 @@
|
||||||
|
import { ButtonEffect } from "../effect-base.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Spinning text animation effect
|
||||||
|
* Makes each character rotate independently
|
||||||
|
*/
|
||||||
|
export class SpinTextEffect extends ButtonEffect {
|
||||||
|
constructor(textLineNumber = 1) {
|
||||||
|
const suffix = textLineNumber === 1 ? "" : "2";
|
||||||
|
super({
|
||||||
|
id: `text-spin${suffix}`,
|
||||||
|
name: `Spinning Text ${textLineNumber}`,
|
||||||
|
type: textLineNumber === 1 ? "text" : "text2",
|
||||||
|
category: textLineNumber === 1 ? "Text Line 1" : "Text Line 2",
|
||||||
|
renderOrder: 8, // Before wave, after rainbow
|
||||||
|
textLineNumber: textLineNumber,
|
||||||
|
});
|
||||||
|
this.textLineNumber = textLineNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
defineControls() {
|
||||||
|
const textLineNumber =
|
||||||
|
this.textLineNumber || this.config?.textLineNumber || 1;
|
||||||
|
const suffix = textLineNumber === 1 ? "" : "2";
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: `animate-text-spin${suffix}`,
|
||||||
|
type: "checkbox",
|
||||||
|
label: "Spinning Animation",
|
||||||
|
defaultValue: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: `spin-speed${suffix}`,
|
||||||
|
type: "range",
|
||||||
|
label: "Spin Speed",
|
||||||
|
defaultValue: 1,
|
||||||
|
min: 1,
|
||||||
|
max: 5,
|
||||||
|
step: 1,
|
||||||
|
showWhen: `animate-text-spin${suffix}`,
|
||||||
|
description: "Speed of character rotation",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: `spin-stagger${suffix}`,
|
||||||
|
type: "range",
|
||||||
|
label: "Spin Stagger",
|
||||||
|
defaultValue: 0.3,
|
||||||
|
min: 0,
|
||||||
|
max: 1,
|
||||||
|
step: 0.1,
|
||||||
|
showWhen: `animate-text-spin${suffix}`,
|
||||||
|
description: "Delay between characters",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
isEnabled(controlValues) {
|
||||||
|
const suffix = this.textLineNumber === 1 ? "" : "2";
|
||||||
|
return controlValues[`animate-text-spin${suffix}`] === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
apply(context, controlValues, animState, renderData) {
|
||||||
|
const suffix = this.textLineNumber === 1 ? "" : "2";
|
||||||
|
const text = controlValues[`button-text${suffix}`] || "";
|
||||||
|
|
||||||
|
if (!text || !controlValues[`text${suffix}-enabled`]) return;
|
||||||
|
if (!animState) return;
|
||||||
|
|
||||||
|
const speed = controlValues[`spin-speed${suffix}`] || 1;
|
||||||
|
const stagger = controlValues[`spin-stagger${suffix}`] || 0.3;
|
||||||
|
const fontSize = controlValues[`font-size${suffix}`] || 14;
|
||||||
|
const fontFamily = controlValues[`font-family${suffix}`] || "Arial";
|
||||||
|
const fontWeight = controlValues[`text${suffix}-bold`] ? "bold" : "normal";
|
||||||
|
const fontStyle = controlValues[`text${suffix}-italic`]
|
||||||
|
? "italic"
|
||||||
|
: "normal";
|
||||||
|
|
||||||
|
context.font = `${fontStyle} ${fontWeight} ${fontSize}px "${fontFamily}"`;
|
||||||
|
context.textAlign = "center";
|
||||||
|
context.textBaseline = "middle";
|
||||||
|
|
||||||
|
// Get text color
|
||||||
|
let fillStyle;
|
||||||
|
const colorType = controlValues[`text${suffix}-color-type`] || "solid";
|
||||||
|
if (colorType === "gradient") {
|
||||||
|
const color1 =
|
||||||
|
controlValues[`text${suffix}-gradient-color1`] || "#ffffff";
|
||||||
|
const color2 =
|
||||||
|
controlValues[`text${suffix}-gradient-color2`] || "#00ffff";
|
||||||
|
const angle =
|
||||||
|
(controlValues[`text${suffix}-gradient-angle`] || 90) * (Math.PI / 180);
|
||||||
|
const centerX = renderData.centerX;
|
||||||
|
const centerY = renderData.centerY;
|
||||||
|
const x1 = centerX + Math.cos(angle) * 20;
|
||||||
|
const y1 = centerY + Math.sin(angle) * 20;
|
||||||
|
const x2 = centerX - Math.cos(angle) * 20;
|
||||||
|
const y2 = centerY - Math.sin(angle) * 20;
|
||||||
|
const gradient = context.createLinearGradient(x1, y1, x2, y2);
|
||||||
|
gradient.addColorStop(0, color1);
|
||||||
|
gradient.addColorStop(1, color2);
|
||||||
|
fillStyle = gradient;
|
||||||
|
} else {
|
||||||
|
fillStyle = controlValues[`text${suffix}-color`] || "#ffffff";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate base position
|
||||||
|
const x = controlValues[`text${suffix}-x`] || 50;
|
||||||
|
const y = controlValues[`text${suffix}-y`] || 50;
|
||||||
|
const baseX = (x / 100) * renderData.width;
|
||||||
|
const baseY = (y / 100) * renderData.height;
|
||||||
|
|
||||||
|
// Split text into grapheme clusters (handles emojis properly)
|
||||||
|
// 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
|
||||||
|
const totalWidth = context.measureText(text).width;
|
||||||
|
let currentX = baseX - totalWidth / 2;
|
||||||
|
|
||||||
|
// Draw each character with rotation
|
||||||
|
for (let i = 0; i < chars.length; i++) {
|
||||||
|
const char = chars[i];
|
||||||
|
const charWidth = context.measureText(char).width;
|
||||||
|
const charCenterX = currentX + charWidth / 2;
|
||||||
|
|
||||||
|
// Calculate rotation for this character
|
||||||
|
const phase = animState.getPhase(speed) + i * stagger * Math.PI * 2;
|
||||||
|
const rotation = phase % (Math.PI * 2);
|
||||||
|
|
||||||
|
context.save();
|
||||||
|
context.translate(charCenterX, baseY);
|
||||||
|
context.rotate(rotation);
|
||||||
|
|
||||||
|
// Apply outline if enabled
|
||||||
|
if (controlValues[`text${suffix}-outline`]) {
|
||||||
|
context.strokeStyle =
|
||||||
|
controlValues[`text${suffix}-outline-color`] || "#000000";
|
||||||
|
context.lineWidth = 2;
|
||||||
|
context.strokeText(char, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
context.fillStyle = fillStyle;
|
||||||
|
context.fillText(char, 0, 0);
|
||||||
|
context.restore();
|
||||||
|
|
||||||
|
currentX += charWidth;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-register effects for both text lines
|
||||||
|
export function register(generator) {
|
||||||
|
generator.registerEffect(new SpinTextEffect(1));
|
||||||
|
generator.registerEffect(new SpinTextEffect(2));
|
||||||
|
}
|
||||||
199
static/js/button-generator/effects/spotlight.js
Normal file
199
static/js/button-generator/effects/spotlight.js
Normal file
|
|
@ -0,0 +1,199 @@
|
||||||
|
/**
|
||||||
|
* EXAMPLE EFFECT
|
||||||
|
*
|
||||||
|
* This is a template for creating new effects.
|
||||||
|
* Copy this file and modify it to create your own custom effects.
|
||||||
|
*
|
||||||
|
* This example creates a "spotlight" effect that highlights a circular area
|
||||||
|
* and darkens the rest of the button.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ButtonEffect } from "../effect-base.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Spotlight Effect
|
||||||
|
* Creates a moving circular spotlight that highlights different areas
|
||||||
|
*/
|
||||||
|
export class SpotlightEffect extends ButtonEffect {
|
||||||
|
constructor() {
|
||||||
|
super({
|
||||||
|
// Unique ID for this effect (used in control IDs)
|
||||||
|
id: "spotlight",
|
||||||
|
|
||||||
|
// Display name shown in UI
|
||||||
|
name: "Spotlight",
|
||||||
|
|
||||||
|
// Effect type determines render order category
|
||||||
|
// Options: 'background', 'border', 'text', 'text2', 'general'
|
||||||
|
type: "general",
|
||||||
|
|
||||||
|
// Category for organizing effects in UI
|
||||||
|
category: "Visual Effects",
|
||||||
|
|
||||||
|
// Render order within type (lower = earlier)
|
||||||
|
// 1-9: backgrounds, 10-19: borders, 20-29: transforms,
|
||||||
|
// 30-49: text, 50-79: overlays, 80-99: post-processing
|
||||||
|
renderOrder: 60,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define UI controls for this effect
|
||||||
|
* These controls will be automatically bound to the generator
|
||||||
|
*/
|
||||||
|
defineControls() {
|
||||||
|
return [
|
||||||
|
// Main enable/disable checkbox
|
||||||
|
{
|
||||||
|
id: "animate-spotlight",
|
||||||
|
type: "checkbox",
|
||||||
|
label: "Spotlight Effect",
|
||||||
|
defaultValue: false,
|
||||||
|
description: "Moving circular spotlight",
|
||||||
|
},
|
||||||
|
|
||||||
|
// Spotlight size control
|
||||||
|
{
|
||||||
|
id: "spotlight-size",
|
||||||
|
type: "range",
|
||||||
|
label: "Spotlight Size",
|
||||||
|
defaultValue: 20,
|
||||||
|
min: 10,
|
||||||
|
max: 50,
|
||||||
|
step: 1,
|
||||||
|
showWhen: "animate-spotlight", // Only show when checkbox is enabled
|
||||||
|
description: "Radius of the spotlight",
|
||||||
|
},
|
||||||
|
|
||||||
|
// Darkness of the vignette
|
||||||
|
{
|
||||||
|
id: "spotlight-darkness",
|
||||||
|
type: "range",
|
||||||
|
label: "Darkness",
|
||||||
|
defaultValue: 1,
|
||||||
|
min: 0,
|
||||||
|
max: 1,
|
||||||
|
step: 0.1,
|
||||||
|
showWhen: "animate-spotlight",
|
||||||
|
description: "How dark the non-spotlight area should be",
|
||||||
|
},
|
||||||
|
|
||||||
|
// Speed of movement
|
||||||
|
{
|
||||||
|
id: "spotlight-speed",
|
||||||
|
type: "range",
|
||||||
|
label: "Movement Speed",
|
||||||
|
defaultValue: 1,
|
||||||
|
min: 1,
|
||||||
|
max: 3,
|
||||||
|
step: 1,
|
||||||
|
showWhen: "animate-spotlight",
|
||||||
|
description: "Speed of spotlight movement",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if this effect should be applied
|
||||||
|
* @param {Object} controlValues - Current values of all controls
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
isEnabled(controlValues) {
|
||||||
|
return controlValues["animate-spotlight"] === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply the effect to the canvas
|
||||||
|
*
|
||||||
|
* @param {CanvasRenderingContext2D} context - Canvas 2D rendering context
|
||||||
|
* @param {Object} controlValues - Current values of all controls
|
||||||
|
* @param {AnimationState|null} animState - Animation state (null for static render)
|
||||||
|
* @param {Object} renderData - Render information: { width, height, centerX, centerY }
|
||||||
|
*/
|
||||||
|
apply(context, controlValues, animState, renderData) {
|
||||||
|
// Skip if no animation (spotlight needs movement)
|
||||||
|
if (!animState) return;
|
||||||
|
|
||||||
|
// Get control values
|
||||||
|
const size = controlValues["spotlight-size"] || 20;
|
||||||
|
const darkness = controlValues["spotlight-darkness"] || 0.5;
|
||||||
|
const speed = controlValues["spotlight-speed"] || 1;
|
||||||
|
|
||||||
|
// Calculate spotlight position
|
||||||
|
// Move in a circular pattern using animation phase
|
||||||
|
const phase = animState.getPhase(speed);
|
||||||
|
const spotX = renderData.centerX + Math.cos(phase) * 20;
|
||||||
|
const spotY = renderData.centerY + Math.sin(phase) * 10;
|
||||||
|
|
||||||
|
// Create radial gradient for spotlight effect
|
||||||
|
const gradient = context.createRadialGradient(
|
||||||
|
spotX,
|
||||||
|
spotY,
|
||||||
|
0, // Inner circle (center of spotlight)
|
||||||
|
spotX,
|
||||||
|
spotY,
|
||||||
|
size, // Outer circle (edge of spotlight)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Center is transparent (spotlight is bright)
|
||||||
|
gradient.addColorStop(0, `rgba(0, 0, 0, 0)`);
|
||||||
|
// Edge fades to dark
|
||||||
|
gradient.addColorStop(0.5, `rgba(0, 0, 0, ${darkness * 0.3})`);
|
||||||
|
gradient.addColorStop(1, `rgba(0, 0, 0, ${darkness})`);
|
||||||
|
|
||||||
|
// Apply the gradient as an overlay
|
||||||
|
context.fillStyle = gradient;
|
||||||
|
context.fillRect(0, 0, renderData.width, renderData.height);
|
||||||
|
|
||||||
|
// Optional: Add a bright center dot
|
||||||
|
context.fillStyle = "rgba(255, 255, 255, 0.3)";
|
||||||
|
context.beginPath();
|
||||||
|
context.arc(spotX, spotY, 2, 0, Math.PI * 2);
|
||||||
|
context.fill();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional: Add helper methods for your effect
|
||||||
|
*/
|
||||||
|
calculateSpotlightPath(progress, width, height) {
|
||||||
|
// Example helper method
|
||||||
|
return {
|
||||||
|
x: width * progress,
|
||||||
|
y: height / 2,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registration function
|
||||||
|
* This is called to add the effect to the generator
|
||||||
|
*
|
||||||
|
* @param {ButtonGenerator} generator - The button generator instance
|
||||||
|
*/
|
||||||
|
export function register(generator) {
|
||||||
|
generator.registerEffect(new SpotlightEffect());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* USAGE:
|
||||||
|
*
|
||||||
|
* 1. Copy this file to a new name (e.g., my-effect.js)
|
||||||
|
* 2. Modify the class name, id, and effect logic
|
||||||
|
* 3. Import in main.js:
|
||||||
|
* import * as myEffect from './effects/my-effect.js';
|
||||||
|
* 4. Register in setupApp():
|
||||||
|
* myEffect.register(generator);
|
||||||
|
* 5. Add HTML controls with matching IDs
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TIPS:
|
||||||
|
*
|
||||||
|
* - Use animState.progress for linear animations (0 to 1)
|
||||||
|
* - Use animState.getPhase(speed) for periodic animations (0 to 2π)
|
||||||
|
* - Use Math.sin/cos for smooth periodic motion
|
||||||
|
* - Check if (!animState) at the start if your effect requires animation
|
||||||
|
* - The context is automatically saved/restored, so feel free to transform
|
||||||
|
* - Use renderData for canvas dimensions and center point
|
||||||
|
* - Look at existing effects for more examples
|
||||||
|
*/
|
||||||
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));
|
||||||
|
}
|
||||||
243
static/js/button-generator/effects/text-standard.js
Normal file
243
static/js/button-generator/effects/text-standard.js
Normal file
|
|
@ -0,0 +1,243 @@
|
||||||
|
import { ButtonEffect } from "../effect-base.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Standard text rendering effect
|
||||||
|
* Renders static text (when no animations are active)
|
||||||
|
*/
|
||||||
|
export class StandardTextEffect extends ButtonEffect {
|
||||||
|
constructor(textLineNumber = 1) {
|
||||||
|
const suffix = textLineNumber === 1 ? "" : "2";
|
||||||
|
super({
|
||||||
|
id: `text-standard${suffix}`,
|
||||||
|
name: `Standard Text ${textLineNumber}`,
|
||||||
|
type: textLineNumber === 1 ? "text" : "text2",
|
||||||
|
category: textLineNumber === 1 ? "Text Line 1" : "Text Line 2",
|
||||||
|
renderOrder: 20, // After animations
|
||||||
|
textLineNumber: textLineNumber, // Pass through config so defineControls can access it
|
||||||
|
});
|
||||||
|
this.textLineNumber = textLineNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
defineControls() {
|
||||||
|
// Access from config since this is called before constructor completes
|
||||||
|
const textLineNumber =
|
||||||
|
this.textLineNumber || this.config?.textLineNumber || 1;
|
||||||
|
const suffix = textLineNumber === 1 ? "" : "2";
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: `button-text${suffix}`,
|
||||||
|
type: "text",
|
||||||
|
label: `Text Line ${textLineNumber}`,
|
||||||
|
defaultValue: textLineNumber === 1 ? "RITUAL.SH" : "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: `font-size${suffix}`,
|
||||||
|
type: "range",
|
||||||
|
label: "Font Size",
|
||||||
|
min: 6,
|
||||||
|
max: 24,
|
||||||
|
defaultValue: textLineNumber === 1 ? 14 : 12,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: `text${suffix}-x`,
|
||||||
|
type: "range",
|
||||||
|
label: "Horizontal Position",
|
||||||
|
min: 0,
|
||||||
|
max: 100,
|
||||||
|
defaultValue: 50,
|
||||||
|
description: "Percentage from left",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: `text${suffix}-y`,
|
||||||
|
type: "range",
|
||||||
|
label: "Vertical Position",
|
||||||
|
min: 0,
|
||||||
|
max: 100,
|
||||||
|
defaultValue: textLineNumber === 1 ? 35 : 65,
|
||||||
|
description: "Percentage from top",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: `text${suffix}-color-type`,
|
||||||
|
type: "select",
|
||||||
|
label: "Color Type",
|
||||||
|
defaultValue: "solid",
|
||||||
|
options: [
|
||||||
|
{ value: "solid", label: "Solid Color" },
|
||||||
|
{ value: "gradient", label: "Gradient" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: `text${suffix}-color`,
|
||||||
|
type: "color",
|
||||||
|
label: "Text Color",
|
||||||
|
defaultValue: "#ffffff",
|
||||||
|
showWhen: `text${suffix}-color-type`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: `text${suffix}-gradient-color1`,
|
||||||
|
type: "color",
|
||||||
|
label: "Gradient Color 1",
|
||||||
|
defaultValue: "#ffffff",
|
||||||
|
showWhen: `text${suffix}-color-type`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: `text${suffix}-gradient-color2`,
|
||||||
|
type: "color",
|
||||||
|
label: "Gradient Color 2",
|
||||||
|
defaultValue: "#00ffff",
|
||||||
|
showWhen: `text${suffix}-color-type`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: `text${suffix}-gradient-angle`,
|
||||||
|
type: "range",
|
||||||
|
label: "Gradient Angle",
|
||||||
|
min: 0,
|
||||||
|
max: 360,
|
||||||
|
defaultValue: 0,
|
||||||
|
showWhen: `text${suffix}-color-type`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: `text${suffix}-outline`,
|
||||||
|
type: "checkbox",
|
||||||
|
label: "Outline",
|
||||||
|
defaultValue: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: `outline${suffix}-color`,
|
||||||
|
type: "color",
|
||||||
|
label: "Outline Color",
|
||||||
|
defaultValue: "#000000",
|
||||||
|
showWhen: `text${suffix}-outline`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: `font-family${suffix}`,
|
||||||
|
type: "select",
|
||||||
|
label: "Font",
|
||||||
|
defaultValue: "Lato",
|
||||||
|
options: [
|
||||||
|
{ value: "Lato", label: "Lato" },
|
||||||
|
{ value: "Roboto", label: "Roboto" },
|
||||||
|
{ value: "Open Sans", label: "Open Sans" },
|
||||||
|
{ value: "Montserrat", label: "Montserrat" },
|
||||||
|
{ value: "Oswald", label: "Oswald" },
|
||||||
|
{ value: "Bebas Neue", label: "Bebas Neue" },
|
||||||
|
{ value: "Roboto Mono", label: "Roboto Mono" },
|
||||||
|
{ value: "VT323", label: "VT323" },
|
||||||
|
{ value: "Press Start 2P", label: "Press Start 2P" },
|
||||||
|
{ value: "DSEG7-Classic", label: "DSEG7" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: `font-bold${suffix}`,
|
||||||
|
type: "checkbox",
|
||||||
|
label: "Bold",
|
||||||
|
defaultValue: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: `font-italic${suffix}`,
|
||||||
|
type: "checkbox",
|
||||||
|
label: "Italic",
|
||||||
|
defaultValue: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
isEnabled(controlValues) {
|
||||||
|
const suffix = this.textLineNumber === 1 ? "" : "2";
|
||||||
|
const text = controlValues[`button-text${suffix}`];
|
||||||
|
|
||||||
|
// Only render if text exists and no animations are active on this text
|
||||||
|
const waveActive = controlValues[`animate-text-wave${suffix}`];
|
||||||
|
const rainbowActive = controlValues[`animate-text-rainbow${suffix}`];
|
||||||
|
const spinActive = controlValues[`animate-text-spin${suffix}`];
|
||||||
|
|
||||||
|
return text && text.trim() !== "" && !waveActive && !rainbowActive && !spinActive;
|
||||||
|
}
|
||||||
|
|
||||||
|
apply(context, controlValues, animState, renderData) {
|
||||||
|
const suffix = this.textLineNumber === 1 ? "" : "2";
|
||||||
|
|
||||||
|
const text = controlValues[`button-text${suffix}`];
|
||||||
|
if (!text) return;
|
||||||
|
|
||||||
|
const fontSize = controlValues[`font-size${suffix}`] || 12;
|
||||||
|
const fontWeight = controlValues[`font-bold${suffix}`] ? "bold" : "normal";
|
||||||
|
const fontStyle = controlValues[`font-italic${suffix}`]
|
||||||
|
? "italic"
|
||||||
|
: "normal";
|
||||||
|
const fontFamily = controlValues[`font-family${suffix}`] || "Arial";
|
||||||
|
|
||||||
|
const x = (controlValues[`text${suffix}-x`] / 100) * renderData.width;
|
||||||
|
const y = (controlValues[`text${suffix}-y`] / 100) * renderData.height;
|
||||||
|
|
||||||
|
// Set font
|
||||||
|
context.font = `${fontStyle} ${fontWeight} ${fontSize}px "${fontFamily}"`;
|
||||||
|
context.textAlign = "center";
|
||||||
|
context.textBaseline = "middle";
|
||||||
|
|
||||||
|
// Get colors
|
||||||
|
const colors = this.getTextColors(
|
||||||
|
context,
|
||||||
|
controlValues,
|
||||||
|
text,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
fontSize,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Draw outline if enabled
|
||||||
|
if (controlValues[`text${suffix}-outline`]) {
|
||||||
|
context.strokeStyle = colors.strokeStyle;
|
||||||
|
context.lineWidth = 2;
|
||||||
|
context.strokeText(text, x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw text
|
||||||
|
context.fillStyle = colors.fillStyle;
|
||||||
|
context.fillText(text, x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get text colors (solid or gradient)
|
||||||
|
*/
|
||||||
|
getTextColors(context, controlValues, text, x, y, fontSize) {
|
||||||
|
const suffix = this.textLineNumber === 1 ? "" : "2";
|
||||||
|
const colorType = controlValues[`text${suffix}-color-type`] || "solid";
|
||||||
|
|
||||||
|
let fillStyle, strokeStyle;
|
||||||
|
|
||||||
|
if (colorType === "solid") {
|
||||||
|
fillStyle = controlValues[`text${suffix}-color`] || "#ffffff";
|
||||||
|
strokeStyle = controlValues[`outline${suffix}-color`] || "#000000";
|
||||||
|
} else {
|
||||||
|
// Gradient
|
||||||
|
const angle =
|
||||||
|
(controlValues[`text${suffix}-gradient-angle`] || 0) * (Math.PI / 180);
|
||||||
|
const textWidth = context.measureText(text).width;
|
||||||
|
const x1 = x - textWidth / 2 + (Math.cos(angle) * textWidth) / 2;
|
||||||
|
const y1 = y - fontSize / 2 + (Math.sin(angle) * fontSize) / 2;
|
||||||
|
const x2 = x + textWidth / 2 - (Math.cos(angle) * textWidth) / 2;
|
||||||
|
const y2 = y + fontSize / 2 - (Math.sin(angle) * fontSize) / 2;
|
||||||
|
|
||||||
|
const gradient = context.createLinearGradient(x1, y1, x2, y2);
|
||||||
|
gradient.addColorStop(
|
||||||
|
0,
|
||||||
|
controlValues[`text${suffix}-gradient-color1`] || "#ffffff",
|
||||||
|
);
|
||||||
|
gradient.addColorStop(
|
||||||
|
1,
|
||||||
|
controlValues[`text${suffix}-gradient-color2`] || "#00ffff",
|
||||||
|
);
|
||||||
|
fillStyle = gradient;
|
||||||
|
strokeStyle = controlValues[`outline${suffix}-color`] || "#000000";
|
||||||
|
}
|
||||||
|
|
||||||
|
return { fillStyle, strokeStyle };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-register effect
|
||||||
|
export function register(generator) {
|
||||||
|
generator.registerEffect(new StandardTextEffect(1));
|
||||||
|
generator.registerEffect(new StandardTextEffect(2));
|
||||||
|
}
|
||||||
178
static/js/button-generator/effects/wave-text.js
Normal file
178
static/js/button-generator/effects/wave-text.js
Normal file
|
|
@ -0,0 +1,178 @@
|
||||||
|
import { ButtonEffect } from '../effect-base.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wave text animation effect
|
||||||
|
* Makes text characters wave up and down in a sine wave pattern
|
||||||
|
*/
|
||||||
|
export class WaveTextEffect extends ButtonEffect {
|
||||||
|
constructor(textLineNumber = 1) {
|
||||||
|
const suffix = textLineNumber === 1 ? '' : '2';
|
||||||
|
super({
|
||||||
|
id: `text-wave${suffix}`,
|
||||||
|
name: `Text Wave ${textLineNumber}`,
|
||||||
|
type: textLineNumber === 1 ? 'text' : 'text2',
|
||||||
|
category: textLineNumber === 1 ? 'Text Line 1' : 'Text Line 2',
|
||||||
|
renderOrder: 10,
|
||||||
|
textLineNumber: textLineNumber // Pass through config
|
||||||
|
});
|
||||||
|
this.textLineNumber = textLineNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
defineControls() {
|
||||||
|
const textLineNumber = this.textLineNumber || this.config?.textLineNumber || 1;
|
||||||
|
const suffix = textLineNumber === 1 ? '' : '2';
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: `animate-text-wave${suffix}`,
|
||||||
|
type: 'checkbox',
|
||||||
|
label: 'Wave Animation',
|
||||||
|
defaultValue: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: `wave-amplitude${suffix}`,
|
||||||
|
type: 'range',
|
||||||
|
label: 'Wave Amplitude',
|
||||||
|
defaultValue: 3,
|
||||||
|
min: 1,
|
||||||
|
max: 10,
|
||||||
|
step: 0.5,
|
||||||
|
showWhen: `animate-text-wave${suffix}`,
|
||||||
|
description: 'Height of the wave motion'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: `wave-speed${suffix}`,
|
||||||
|
type: 'range',
|
||||||
|
label: 'Wave Speed',
|
||||||
|
defaultValue: 1,
|
||||||
|
min: 0.1,
|
||||||
|
max: 3,
|
||||||
|
step: 0.1,
|
||||||
|
showWhen: `animate-text-wave${suffix}`,
|
||||||
|
description: 'Speed of the wave animation'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
isEnabled(controlValues) {
|
||||||
|
const suffix = this.textLineNumber === 1 ? '' : '2';
|
||||||
|
return controlValues[`animate-text-wave${suffix}`] === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
apply(context, controlValues, animState, renderData) {
|
||||||
|
if (!animState) return; // Wave requires animation
|
||||||
|
|
||||||
|
const suffix = this.textLineNumber === 1 ? '' : '2';
|
||||||
|
|
||||||
|
// Get text configuration
|
||||||
|
const text = controlValues[`button-text${suffix}`] || '';
|
||||||
|
const enabled = controlValues[`text${suffix}-enabled`];
|
||||||
|
if (!text || !enabled) return;
|
||||||
|
|
||||||
|
const fontSize = controlValues[`font-size${suffix}`] || 12;
|
||||||
|
const fontWeight = controlValues[`font-bold${suffix}`] ? 'bold' : 'normal';
|
||||||
|
const fontStyle = controlValues[`font-italic${suffix}`] ? 'italic' : 'normal';
|
||||||
|
const fontFamily = controlValues[`font-family${suffix}`] || 'Arial';
|
||||||
|
|
||||||
|
const baseX = (controlValues[`text${suffix}-x`] / 100) * renderData.width;
|
||||||
|
const baseY = (controlValues[`text${suffix}-y`] / 100) * renderData.height;
|
||||||
|
|
||||||
|
const amplitude = controlValues[`wave-amplitude${suffix}`] || 3;
|
||||||
|
const speed = controlValues[`wave-speed${suffix}`] || 1;
|
||||||
|
|
||||||
|
// Set font
|
||||||
|
context.font = `${fontStyle} ${fontWeight} ${fontSize}px "${fontFamily}"`;
|
||||||
|
context.textAlign = 'center';
|
||||||
|
context.textBaseline = 'middle';
|
||||||
|
|
||||||
|
// Get colors
|
||||||
|
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
|
||||||
|
const totalWidth = context.measureText(text).width;
|
||||||
|
let currentX = baseX - totalWidth / 2;
|
||||||
|
|
||||||
|
// Draw each character with wave offset
|
||||||
|
for (let i = 0; i < chars.length; i++) {
|
||||||
|
const char = chars[i];
|
||||||
|
const charWidth = context.measureText(char).width;
|
||||||
|
|
||||||
|
// Calculate wave offset for this character
|
||||||
|
const phase = animState.getPhase(speed);
|
||||||
|
const charOffset = i / chars.length;
|
||||||
|
const waveY = Math.sin(phase + charOffset * Math.PI * 2) * amplitude;
|
||||||
|
|
||||||
|
const charX = currentX + charWidth / 2;
|
||||||
|
const charY = baseY + waveY;
|
||||||
|
|
||||||
|
// Draw outline if enabled
|
||||||
|
if (controlValues[`text${suffix}-outline`]) {
|
||||||
|
context.strokeStyle = colors.strokeStyle;
|
||||||
|
context.lineWidth = 2;
|
||||||
|
context.strokeText(char, charX, charY);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw character
|
||||||
|
context.fillStyle = colors.fillStyle;
|
||||||
|
context.fillText(char, charX, charY);
|
||||||
|
|
||||||
|
currentX += charWidth;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get text colors (solid, gradient, or rainbow)
|
||||||
|
*/
|
||||||
|
getTextColors(context, controlValues, text, x, y, fontSize, animState) {
|
||||||
|
const suffix = this.textLineNumber === 1 ? '' : '2';
|
||||||
|
|
||||||
|
let fillStyle, strokeStyle;
|
||||||
|
|
||||||
|
// Check if rainbow text is also enabled
|
||||||
|
if (animState && controlValues[`animate-text-rainbow${suffix}`]) {
|
||||||
|
const speed = controlValues[`text-rainbow-speed${suffix}`] || 1;
|
||||||
|
const hue = (animState.progress * speed * 360) % 360;
|
||||||
|
fillStyle = `hsl(${hue}, 80%, 60%)`;
|
||||||
|
strokeStyle = `hsl(${hue}, 80%, 30%)`;
|
||||||
|
} else {
|
||||||
|
const colorType = controlValues[`text${suffix}-color-type`] || 'solid';
|
||||||
|
|
||||||
|
if (colorType === 'solid') {
|
||||||
|
fillStyle = controlValues[`text${suffix}-color`] || '#ffffff';
|
||||||
|
strokeStyle = controlValues[`outline${suffix}-color`] || '#000000';
|
||||||
|
} else {
|
||||||
|
// Gradient
|
||||||
|
const angle = (controlValues[`text${suffix}-gradient-angle`] || 0) * (Math.PI / 180);
|
||||||
|
const textWidth = context.measureText(text).width;
|
||||||
|
const x1 = x - textWidth / 2 + (Math.cos(angle) * textWidth) / 2;
|
||||||
|
const y1 = y - fontSize / 2 + (Math.sin(angle) * fontSize) / 2;
|
||||||
|
const x2 = x + textWidth / 2 - (Math.cos(angle) * textWidth) / 2;
|
||||||
|
const y2 = y + fontSize / 2 - (Math.sin(angle) * fontSize) / 2;
|
||||||
|
|
||||||
|
const gradient = context.createLinearGradient(x1, y1, x2, y2);
|
||||||
|
gradient.addColorStop(0, controlValues[`text${suffix}-gradient-color1`] || '#ffffff');
|
||||||
|
gradient.addColorStop(1, controlValues[`text${suffix}-gradient-color2`] || '#00ffff');
|
||||||
|
fillStyle = gradient;
|
||||||
|
strokeStyle = controlValues[`outline${suffix}-color`] || '#000000';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { fillStyle, strokeStyle };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-register effect
|
||||||
|
export function register(generator) {
|
||||||
|
generator.registerEffect(new WaveTextEffect(1));
|
||||||
|
generator.registerEffect(new WaveTextEffect(2));
|
||||||
|
}
|
||||||
363
static/js/button-generator/main.js
Normal file
363
static/js/button-generator/main.js
Normal file
|
|
@ -0,0 +1,363 @@
|
||||||
|
/**
|
||||||
|
* Button Generator - Main Application File
|
||||||
|
*
|
||||||
|
* This demonstrates how to use the modular button generator system.
|
||||||
|
* Effects are imported and registered with the generator.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ButtonGenerator } from "./button-generator-core.js";
|
||||||
|
import { UIBuilder } from "./ui-builder.js";
|
||||||
|
|
||||||
|
// Import effects (each effect file exports a register() function)
|
||||||
|
import * as solidBg from "./effects/background-solid.js";
|
||||||
|
import * as gradientBg from "./effects/background-gradient.js";
|
||||||
|
import * as textureBg from "./effects/background-texture.js";
|
||||||
|
import * as emojiWallpaper from "./effects/background-emoji-wallpaper.js";
|
||||||
|
import * as rainbowBg from "./effects/background-rainbow.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 standardText from "./effects/text-standard.js";
|
||||||
|
import * as textShadow from "./effects/text-shadow.js";
|
||||||
|
import * as waveText from "./effects/wave-text.js";
|
||||||
|
import * as rainbowText from "./effects/rainbow-text.js";
|
||||||
|
import * as spinText from "./effects/spin-text.js";
|
||||||
|
import * as glitch from "./effects/glitch.js";
|
||||||
|
import * as pulse from "./effects/pulse.js";
|
||||||
|
import * as shimmer from "./effects/shimmer.js";
|
||||||
|
import * as scanline from "./effects/scanline.js";
|
||||||
|
import * as rgbSplit from "./effects/rgb-split.js";
|
||||||
|
import * as noise from "./effects/noise.js";
|
||||||
|
import * as rotate from "./effects/rotate.js";
|
||||||
|
import * as hologram from "./effects/hologram.js";
|
||||||
|
import * as spotlight from "./effects/spotlight.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the button generator application
|
||||||
|
*/
|
||||||
|
export function init() {
|
||||||
|
// Wait for DOM to be ready
|
||||||
|
if (document.readyState === "loading") {
|
||||||
|
document.addEventListener("DOMContentLoaded", setupApp);
|
||||||
|
} else {
|
||||||
|
setupApp();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup the application
|
||||||
|
*/
|
||||||
|
async function setupApp() {
|
||||||
|
console.log("Initializing Button Generator...");
|
||||||
|
|
||||||
|
// Setup collapsible sections
|
||||||
|
setupCollapsible();
|
||||||
|
|
||||||
|
// Get canvas
|
||||||
|
const canvas = document.getElementById("button-canvas");
|
||||||
|
if (!canvas) {
|
||||||
|
console.error("Canvas element not found!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create button generator
|
||||||
|
const generator = new ButtonGenerator(canvas, {
|
||||||
|
fps: 20,
|
||||||
|
duration: 2,
|
||||||
|
fonts: [
|
||||||
|
"Arial",
|
||||||
|
"Lato",
|
||||||
|
"Roboto",
|
||||||
|
"Open Sans",
|
||||||
|
"Montserrat",
|
||||||
|
"Oswald",
|
||||||
|
"Bebas Neue",
|
||||||
|
"Roboto Mono",
|
||||||
|
"VT323",
|
||||||
|
"Press Start 2P",
|
||||||
|
"DSEG7-Classic",
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Register all effects
|
||||||
|
console.log("Registering effects...");
|
||||||
|
solidBg.register(generator);
|
||||||
|
gradientBg.register(generator);
|
||||||
|
textureBg.register(generator);
|
||||||
|
emojiWallpaper.register(generator);
|
||||||
|
rainbowBg.register(generator);
|
||||||
|
rain.register(generator);
|
||||||
|
starfield.register(generator);
|
||||||
|
//bubbles.register(generator);
|
||||||
|
aurora.register(generator);
|
||||||
|
fire.register(generator);
|
||||||
|
border.register(generator);
|
||||||
|
standardText.register(generator);
|
||||||
|
textShadow.register(generator);
|
||||||
|
waveText.register(generator);
|
||||||
|
rainbowText.register(generator);
|
||||||
|
spinText.register(generator);
|
||||||
|
glitch.register(generator);
|
||||||
|
pulse.register(generator);
|
||||||
|
shimmer.register(generator);
|
||||||
|
scanline.register(generator);
|
||||||
|
rgbSplit.register(generator);
|
||||||
|
noise.register(generator);
|
||||||
|
rotate.register(generator);
|
||||||
|
hologram.register(generator);
|
||||||
|
spotlight.register(generator);
|
||||||
|
|
||||||
|
console.log(`Registered ${generator.getAllEffects().length} effects`);
|
||||||
|
|
||||||
|
// Build UI from effects
|
||||||
|
console.log("Building UI...");
|
||||||
|
const controlsContainer = document.querySelector(".controls-section");
|
||||||
|
if (!controlsContainer) {
|
||||||
|
console.error("Controls container not found!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uiBuilder = new UIBuilder(controlsContainer);
|
||||||
|
uiBuilder.buildUI(generator.getAllEffects());
|
||||||
|
uiBuilder.setupConditionalVisibility();
|
||||||
|
|
||||||
|
// Preload fonts
|
||||||
|
console.log("Loading fonts...");
|
||||||
|
await generator.preloadFonts();
|
||||||
|
|
||||||
|
// Bind controls (after UI is built)
|
||||||
|
generator.bindControls();
|
||||||
|
|
||||||
|
// Setup additional UI handlers
|
||||||
|
setupUIHandlers(generator);
|
||||||
|
|
||||||
|
// Setup download button
|
||||||
|
setupDownloadButton(generator);
|
||||||
|
|
||||||
|
// Setup presets
|
||||||
|
setupPresets(generator);
|
||||||
|
|
||||||
|
// Initial draw
|
||||||
|
generator.updatePreview();
|
||||||
|
|
||||||
|
console.log("Button Generator ready!");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup collapsible section functionality
|
||||||
|
*/
|
||||||
|
function setupCollapsible() {
|
||||||
|
const headers = document.querySelectorAll(".control-group-header");
|
||||||
|
console.log("Found", headers.length, "collapsible headers");
|
||||||
|
|
||||||
|
headers.forEach((header) => {
|
||||||
|
header.addEventListener("click", () => {
|
||||||
|
const controlGroup = header.closest(".control-group");
|
||||||
|
if (controlGroup) {
|
||||||
|
controlGroup.classList.toggle("collapsed");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup UI handlers for conditional visibility
|
||||||
|
*/
|
||||||
|
function setupUIHandlers(generator) {
|
||||||
|
// Note: Conditional visibility is now handled automatically by UIBuilder.setupConditionalVisibility()
|
||||||
|
// This function is kept for any additional custom handlers if needed in the future
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup download button
|
||||||
|
*/
|
||||||
|
function setupDownloadButton(generator) {
|
||||||
|
const downloadBtn = document.getElementById("download-button");
|
||||||
|
if (!downloadBtn) return;
|
||||||
|
|
||||||
|
downloadBtn.addEventListener("click", async () => {
|
||||||
|
const originalText = downloadBtn.textContent;
|
||||||
|
downloadBtn.disabled = true;
|
||||||
|
downloadBtn.textContent = "Generating GIF...";
|
||||||
|
|
||||||
|
try {
|
||||||
|
const blob = await generator.exportAsGif((progress, stage) => {
|
||||||
|
if (stage === "generating") {
|
||||||
|
const percent = Math.round(progress * 100);
|
||||||
|
downloadBtn.textContent = `Generating: ${percent}%`;
|
||||||
|
} else if (stage === "encoding") {
|
||||||
|
const percent = Math.round(progress * 100);
|
||||||
|
downloadBtn.textContent = `Encoding: ${percent}%`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Download the blob
|
||||||
|
const link = document.createElement("a");
|
||||||
|
link.download = "button-88x31.gif";
|
||||||
|
link.href = URL.createObjectURL(blob);
|
||||||
|
link.click();
|
||||||
|
URL.revokeObjectURL(link.href);
|
||||||
|
|
||||||
|
downloadBtn.textContent = originalText;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error generating GIF:", error);
|
||||||
|
alert("Error generating GIF. Please try again.");
|
||||||
|
downloadBtn.textContent = originalText;
|
||||||
|
} finally {
|
||||||
|
downloadBtn.disabled = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup preset buttons
|
||||||
|
*/
|
||||||
|
function setupPresets(generator) {
|
||||||
|
// Random preset
|
||||||
|
const randomBtn = document.getElementById("preset-random");
|
||||||
|
if (randomBtn) {
|
||||||
|
randomBtn.addEventListener("click", () => {
|
||||||
|
applyRandomPreset();
|
||||||
|
generator.updatePreview();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Classic preset
|
||||||
|
const classicBtn = document.getElementById("preset-classic");
|
||||||
|
if (classicBtn) {
|
||||||
|
classicBtn.addEventListener("click", () => {
|
||||||
|
applyClassicPreset();
|
||||||
|
generator.updatePreview();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modern preset
|
||||||
|
const modernBtn = document.getElementById("preset-modern");
|
||||||
|
if (modernBtn) {
|
||||||
|
modernBtn.addEventListener("click", () => {
|
||||||
|
applyModernPreset();
|
||||||
|
generator.updatePreview();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply random preset
|
||||||
|
*/
|
||||||
|
function applyRandomPreset() {
|
||||||
|
const randomColor = () =>
|
||||||
|
"#" +
|
||||||
|
Math.floor(Math.random() * 16777215)
|
||||||
|
.toString(16)
|
||||||
|
.padStart(6, "0");
|
||||||
|
|
||||||
|
// Random background
|
||||||
|
const bgTypeEl = document.getElementById("bg-type");
|
||||||
|
if (bgTypeEl) {
|
||||||
|
bgTypeEl.value = ["solid", "gradient", "texture"][
|
||||||
|
Math.floor(Math.random() * 3)
|
||||||
|
];
|
||||||
|
bgTypeEl.dispatchEvent(new Event("change"));
|
||||||
|
}
|
||||||
|
|
||||||
|
setControlValue("bg-color", randomColor());
|
||||||
|
setControlValue("gradient-color1", randomColor());
|
||||||
|
setControlValue("gradient-color2", randomColor());
|
||||||
|
setControlValue("gradient-angle", Math.floor(Math.random() * 360));
|
||||||
|
|
||||||
|
// Random text colors
|
||||||
|
setControlValue("text-color", randomColor());
|
||||||
|
setControlValue("text-gradient-color1", randomColor());
|
||||||
|
setControlValue("text-gradient-color2", randomColor());
|
||||||
|
setControlValue("text2-color", randomColor());
|
||||||
|
setControlValue("text2-gradient-color1", randomColor());
|
||||||
|
setControlValue("text2-gradient-color2", randomColor());
|
||||||
|
|
||||||
|
// Random border
|
||||||
|
setControlValue("border-color", randomColor());
|
||||||
|
setControlValue("border-width", Math.floor(Math.random() * 6));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply classic 90s preset
|
||||||
|
*/
|
||||||
|
function applyClassicPreset() {
|
||||||
|
setControlValue("bg-type", "gradient");
|
||||||
|
setControlValue("gradient-color1", "#6e6e6eff");
|
||||||
|
setControlValue("gradient-color2", "#979797");
|
||||||
|
setControlValue("gradient-angle", 90);
|
||||||
|
|
||||||
|
setControlValue("text-color-type", "solid");
|
||||||
|
setControlValue("text-color", "#000000");
|
||||||
|
setControlValue("text2-color-type", "solid");
|
||||||
|
setControlValue("text2-color", "#000000");
|
||||||
|
|
||||||
|
setControlValue("border-width", 2);
|
||||||
|
setControlValue("border-color", "#000000");
|
||||||
|
setControlValue("border-style", "outset");
|
||||||
|
|
||||||
|
setControlValue("font-family", "VT323");
|
||||||
|
setControlValue("font-family2", "VT323");
|
||||||
|
|
||||||
|
document.getElementById("bg-type")?.dispatchEvent(new Event("change"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply modern preset
|
||||||
|
*/
|
||||||
|
function applyModernPreset() {
|
||||||
|
setControlValue("bg-type", "gradient");
|
||||||
|
setControlValue("gradient-color1", "#0a0a0a");
|
||||||
|
setControlValue("gradient-color2", "#1a0a2e");
|
||||||
|
setControlValue("gradient-angle", 135);
|
||||||
|
|
||||||
|
setControlValue("text-color-type", "gradient");
|
||||||
|
setControlValue("text-gradient-color1", "#00ffaa");
|
||||||
|
setControlValue("text-gradient-color2", "#00ffff");
|
||||||
|
setControlValue("text-gradient-angle", 90);
|
||||||
|
|
||||||
|
setControlValue("text2-color-type", "gradient");
|
||||||
|
setControlValue("text2-gradient-color1", "#ff00ff");
|
||||||
|
setControlValue("text2-gradient-color2", "#ff6600");
|
||||||
|
|
||||||
|
setControlValue("border-width", 1);
|
||||||
|
setControlValue("border-color", "#00ffaa");
|
||||||
|
setControlValue("border-style", "solid");
|
||||||
|
|
||||||
|
setControlValue("font-family", "Roboto Mono");
|
||||||
|
setControlValue("font-family2", "Roboto Mono");
|
||||||
|
|
||||||
|
document.getElementById("bg-type")?.dispatchEvent(new Event("change"));
|
||||||
|
document
|
||||||
|
.getElementById("text-color-type")
|
||||||
|
?.dispatchEvent(new Event("change"));
|
||||||
|
document
|
||||||
|
.getElementById("text2-color-type")
|
||||||
|
?.dispatchEvent(new Event("change"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to set control value
|
||||||
|
*/
|
||||||
|
function setControlValue(id, value) {
|
||||||
|
const el = document.getElementById(id);
|
||||||
|
if (el) {
|
||||||
|
if (el.type === "checkbox") {
|
||||||
|
el.checked = value;
|
||||||
|
} else {
|
||||||
|
el.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update value display if it exists
|
||||||
|
const valueDisplay = document.getElementById(id + "-value");
|
||||||
|
if (valueDisplay) {
|
||||||
|
valueDisplay.textContent = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-initialize when imported
|
||||||
|
init();
|
||||||
506
static/js/button-generator/ui-builder.js
Normal file
506
static/js/button-generator/ui-builder.js
Normal file
|
|
@ -0,0 +1,506 @@
|
||||||
|
/**
|
||||||
|
* UI Builder - Dynamically generates control UI from effect definitions
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class UIBuilder {
|
||||||
|
constructor(containerElement) {
|
||||||
|
this.container = containerElement;
|
||||||
|
this.controlGroups = new Map(); // category -> { element, controls }
|
||||||
|
this.tooltip = null;
|
||||||
|
this.tooltipTimeout = null;
|
||||||
|
this.setupTooltip();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create and setup the tooltip element
|
||||||
|
*/
|
||||||
|
setupTooltip() {
|
||||||
|
// Wait for DOM to be ready
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
this.createTooltipElement();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.createTooltipElement();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createTooltipElement() {
|
||||||
|
this.tooltip = document.createElement('div');
|
||||||
|
this.tooltip.className = 'control-tooltip';
|
||||||
|
this.tooltip.style.cssText = `
|
||||||
|
position: fixed;
|
||||||
|
background: linear-gradient(135deg, rgba(0, 120, 200, 0.98) 0%, rgba(0, 100, 180, 0.98) 100%);
|
||||||
|
color: #fff;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 10000;
|
||||||
|
max-width: 250px;
|
||||||
|
box-shadow: 0 0 20px rgba(0, 150, 255, 0.5), 0 4px 12px rgba(0, 0, 0, 0.4);
|
||||||
|
border: 1px solid rgba(0, 150, 255, 0.6);
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.15s ease;
|
||||||
|
line-height: 1.4;
|
||||||
|
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
|
||||||
|
`;
|
||||||
|
document.body.appendChild(this.tooltip);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show tooltip for an element
|
||||||
|
*/
|
||||||
|
showTooltip(element, text) {
|
||||||
|
if (!text || !this.tooltip) return;
|
||||||
|
|
||||||
|
clearTimeout(this.tooltipTimeout);
|
||||||
|
|
||||||
|
this.tooltip.textContent = text;
|
||||||
|
this.tooltip.style.opacity = '1';
|
||||||
|
|
||||||
|
// Position tooltip above the element
|
||||||
|
const rect = element.getBoundingClientRect();
|
||||||
|
|
||||||
|
// Set initial position to measure
|
||||||
|
this.tooltip.style.left = '0px';
|
||||||
|
this.tooltip.style.top = '0px';
|
||||||
|
this.tooltip.style.visibility = 'hidden';
|
||||||
|
this.tooltip.style.display = 'block';
|
||||||
|
|
||||||
|
const tooltipRect = this.tooltip.getBoundingClientRect();
|
||||||
|
|
||||||
|
this.tooltip.style.visibility = 'visible';
|
||||||
|
|
||||||
|
let left = rect.left + rect.width / 2 - tooltipRect.width / 2;
|
||||||
|
let top = rect.top - tooltipRect.height - 10;
|
||||||
|
|
||||||
|
// Keep tooltip on screen
|
||||||
|
const padding = 10;
|
||||||
|
if (left < padding) left = padding;
|
||||||
|
if (left + tooltipRect.width > window.innerWidth - padding) {
|
||||||
|
left = window.innerWidth - tooltipRect.width - padding;
|
||||||
|
}
|
||||||
|
if (top < padding) {
|
||||||
|
top = rect.bottom + 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.tooltip.style.left = `${left}px`;
|
||||||
|
this.tooltip.style.top = `${top}px`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hide tooltip
|
||||||
|
*/
|
||||||
|
hideTooltip() {
|
||||||
|
if (!this.tooltip) return;
|
||||||
|
clearTimeout(this.tooltipTimeout);
|
||||||
|
this.tooltipTimeout = setTimeout(() => {
|
||||||
|
this.tooltip.style.opacity = '0';
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add tooltip handlers to an element
|
||||||
|
*/
|
||||||
|
addTooltipHandlers(element, description) {
|
||||||
|
if (!description) return;
|
||||||
|
|
||||||
|
element.addEventListener('mouseenter', () => {
|
||||||
|
this.showTooltip(element, description);
|
||||||
|
});
|
||||||
|
|
||||||
|
element.addEventListener('mouseleave', () => {
|
||||||
|
this.hideTooltip();
|
||||||
|
});
|
||||||
|
|
||||||
|
element.addEventListener('mousemove', () => {
|
||||||
|
// Update position on mouse move for better following
|
||||||
|
if (this.tooltip && this.tooltip.style.opacity === '1') {
|
||||||
|
this.showTooltip(element, description);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the entire UI from registered effects
|
||||||
|
* @param {Array<ButtonEffect>} effects - All registered effects
|
||||||
|
*/
|
||||||
|
buildUI(effects) {
|
||||||
|
// Clear existing content
|
||||||
|
this.container.innerHTML = '';
|
||||||
|
this.controlGroups.clear();
|
||||||
|
|
||||||
|
// Group effects by category
|
||||||
|
const categorized = this.categorizeEffects(effects);
|
||||||
|
|
||||||
|
// Create control groups for each category
|
||||||
|
for (const [category, categoryEffects] of categorized) {
|
||||||
|
this.createControlGroup(category, categoryEffects);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Categorize effects by their category property
|
||||||
|
* @param {Array<ButtonEffect>} effects
|
||||||
|
* @returns {Map<string, Array<ButtonEffect>>}
|
||||||
|
*/
|
||||||
|
categorizeEffects(effects) {
|
||||||
|
const categories = new Map();
|
||||||
|
|
||||||
|
effects.forEach(effect => {
|
||||||
|
const category = effect.category || 'Other';
|
||||||
|
if (!categories.has(category)) {
|
||||||
|
categories.set(category, []);
|
||||||
|
}
|
||||||
|
categories.get(category).push(effect);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sort categories in a logical order
|
||||||
|
const orderedCategories = new Map();
|
||||||
|
const categoryOrder = [
|
||||||
|
'Text Line 1',
|
||||||
|
'Text Line 2',
|
||||||
|
'Background',
|
||||||
|
'Background Animations',
|
||||||
|
'Border',
|
||||||
|
'Visual Effects',
|
||||||
|
'General Effects',
|
||||||
|
'Special Effects'
|
||||||
|
];
|
||||||
|
|
||||||
|
categoryOrder.forEach(cat => {
|
||||||
|
if (categories.has(cat)) {
|
||||||
|
orderedCategories.set(cat, categories.get(cat));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add any remaining categories
|
||||||
|
categories.forEach((effects, cat) => {
|
||||||
|
if (!orderedCategories.has(cat)) {
|
||||||
|
orderedCategories.set(cat, effects);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return orderedCategories;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a collapsible control group
|
||||||
|
* @param {string} category - Category name
|
||||||
|
* @param {Array<ButtonEffect>} effects - Effects in this category
|
||||||
|
*/
|
||||||
|
createControlGroup(category, effects) {
|
||||||
|
const groupDiv = document.createElement('div');
|
||||||
|
groupDiv.className = 'control-group';
|
||||||
|
|
||||||
|
// Create header
|
||||||
|
const header = document.createElement('h3');
|
||||||
|
header.className = 'control-group-header';
|
||||||
|
header.innerHTML = `
|
||||||
|
<span>${category}</span>
|
||||||
|
<span class="toggle-icon">−</span>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Create content container
|
||||||
|
const content = document.createElement('div');
|
||||||
|
content.className = 'control-group-content';
|
||||||
|
|
||||||
|
// Add controls for each effect in this category
|
||||||
|
effects.forEach(effect => {
|
||||||
|
this.addEffectControls(content, effect);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add click handler for collapsing
|
||||||
|
header.addEventListener('click', () => {
|
||||||
|
groupDiv.classList.toggle('collapsed');
|
||||||
|
});
|
||||||
|
|
||||||
|
groupDiv.appendChild(header);
|
||||||
|
groupDiv.appendChild(content);
|
||||||
|
this.container.appendChild(groupDiv);
|
||||||
|
|
||||||
|
this.controlGroups.set(category, { element: groupDiv, effects });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add controls for a single effect
|
||||||
|
* @param {HTMLElement} container - Container to add controls to
|
||||||
|
* @param {ButtonEffect} effect - Effect to create controls for
|
||||||
|
*/
|
||||||
|
addEffectControls(container, effect) {
|
||||||
|
effect.controls.forEach(control => {
|
||||||
|
const controlEl = this.createControl(control);
|
||||||
|
if (controlEl) {
|
||||||
|
container.appendChild(controlEl);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a single control element
|
||||||
|
* @param {Object} controlDef - Control definition from effect
|
||||||
|
* @returns {HTMLElement}
|
||||||
|
*/
|
||||||
|
createControl(controlDef) {
|
||||||
|
const { id, type, label, defaultValue, min, max, step, options, showWhen, description } = controlDef;
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'checkbox':
|
||||||
|
return this.createCheckbox(id, label, defaultValue, showWhen, description);
|
||||||
|
|
||||||
|
case 'range':
|
||||||
|
return this.createRange(id, label, defaultValue, min, max, step, description, showWhen);
|
||||||
|
|
||||||
|
case 'color':
|
||||||
|
return this.createColor(id, label, defaultValue, showWhen, description);
|
||||||
|
|
||||||
|
case 'select':
|
||||||
|
return this.createSelect(id, label, defaultValue, options, showWhen, description);
|
||||||
|
|
||||||
|
case 'text':
|
||||||
|
return this.createTextInput(id, label, defaultValue, showWhen, description);
|
||||||
|
|
||||||
|
default:
|
||||||
|
console.warn(`Unknown control type: ${type}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a checkbox control
|
||||||
|
*/
|
||||||
|
createCheckbox(id, label, defaultValue, showWhen, description) {
|
||||||
|
const wrapper = document.createElement('label');
|
||||||
|
wrapper.className = 'checkbox-label';
|
||||||
|
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.type = 'checkbox';
|
||||||
|
input.id = id;
|
||||||
|
input.checked = defaultValue || false;
|
||||||
|
|
||||||
|
const span = document.createElement('span');
|
||||||
|
span.textContent = label;
|
||||||
|
|
||||||
|
wrapper.appendChild(input);
|
||||||
|
wrapper.appendChild(span);
|
||||||
|
|
||||||
|
if (showWhen) {
|
||||||
|
wrapper.style.display = 'none';
|
||||||
|
wrapper.dataset.showWhen = showWhen;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add tooltip handlers to the label wrapper
|
||||||
|
this.addTooltipHandlers(wrapper, description);
|
||||||
|
|
||||||
|
return wrapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a range slider control
|
||||||
|
*/
|
||||||
|
createRange(id, label, defaultValue, min, max, step, description, showWhen) {
|
||||||
|
const container = document.createElement('div');
|
||||||
|
|
||||||
|
const labelEl = document.createElement('label');
|
||||||
|
labelEl.htmlFor = id;
|
||||||
|
labelEl.innerHTML = `${label}: <span id="${id}-value">${defaultValue}</span>`;
|
||||||
|
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.type = 'range';
|
||||||
|
input.id = id;
|
||||||
|
input.min = min !== undefined ? min : 0;
|
||||||
|
input.max = max !== undefined ? max : 100;
|
||||||
|
input.value = defaultValue !== undefined ? defaultValue : 50;
|
||||||
|
if (step !== undefined) {
|
||||||
|
input.step = step;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update value display on input
|
||||||
|
input.addEventListener('input', () => {
|
||||||
|
const valueDisplay = document.getElementById(`${id}-value`);
|
||||||
|
if (valueDisplay) {
|
||||||
|
valueDisplay.textContent = input.value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
container.appendChild(labelEl);
|
||||||
|
container.appendChild(input);
|
||||||
|
|
||||||
|
if (showWhen) {
|
||||||
|
container.style.display = 'none';
|
||||||
|
container.dataset.showWhen = showWhen;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add tooltip handlers to the label
|
||||||
|
this.addTooltipHandlers(labelEl, description);
|
||||||
|
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a color picker control
|
||||||
|
*/
|
||||||
|
createColor(id, label, defaultValue, showWhen, description) {
|
||||||
|
const container = document.createElement('div');
|
||||||
|
|
||||||
|
const labelEl = document.createElement('label');
|
||||||
|
labelEl.htmlFor = id;
|
||||||
|
labelEl.textContent = label;
|
||||||
|
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.type = 'color';
|
||||||
|
input.id = id;
|
||||||
|
input.value = defaultValue || '#ffffff';
|
||||||
|
|
||||||
|
container.appendChild(labelEl);
|
||||||
|
container.appendChild(input);
|
||||||
|
|
||||||
|
if (showWhen) {
|
||||||
|
container.style.display = 'none';
|
||||||
|
container.dataset.showWhen = showWhen;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add tooltip handlers to the label
|
||||||
|
this.addTooltipHandlers(labelEl, description);
|
||||||
|
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a select dropdown control
|
||||||
|
*/
|
||||||
|
createSelect(id, label, defaultValue, options, showWhen, description) {
|
||||||
|
const container = document.createElement('div');
|
||||||
|
|
||||||
|
const labelEl = document.createElement('label');
|
||||||
|
labelEl.htmlFor = id;
|
||||||
|
labelEl.textContent = label;
|
||||||
|
|
||||||
|
const select = document.createElement('select');
|
||||||
|
select.id = id;
|
||||||
|
|
||||||
|
options.forEach(opt => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = opt.value;
|
||||||
|
option.textContent = opt.label;
|
||||||
|
if (opt.value === defaultValue) {
|
||||||
|
option.selected = true;
|
||||||
|
}
|
||||||
|
select.appendChild(option);
|
||||||
|
});
|
||||||
|
|
||||||
|
container.appendChild(labelEl);
|
||||||
|
container.appendChild(select);
|
||||||
|
|
||||||
|
if (showWhen) {
|
||||||
|
container.style.display = 'none';
|
||||||
|
container.dataset.showWhen = showWhen;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add tooltip handlers to the label
|
||||||
|
this.addTooltipHandlers(labelEl, description);
|
||||||
|
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a text input control
|
||||||
|
*/
|
||||||
|
createTextInput(id, label, defaultValue, showWhen, description) {
|
||||||
|
const container = document.createElement('div');
|
||||||
|
|
||||||
|
const labelEl = document.createElement('label');
|
||||||
|
labelEl.htmlFor = id;
|
||||||
|
labelEl.textContent = label;
|
||||||
|
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.type = 'text';
|
||||||
|
input.id = id;
|
||||||
|
input.value = defaultValue || '';
|
||||||
|
input.maxLength = 20;
|
||||||
|
|
||||||
|
container.appendChild(labelEl);
|
||||||
|
container.appendChild(input);
|
||||||
|
|
||||||
|
if (showWhen) {
|
||||||
|
container.style.display = 'none';
|
||||||
|
container.dataset.showWhen = showWhen;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add tooltip handlers to the label
|
||||||
|
this.addTooltipHandlers(labelEl, description);
|
||||||
|
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup conditional visibility for controls
|
||||||
|
* Should be called after all controls are created
|
||||||
|
*/
|
||||||
|
setupConditionalVisibility() {
|
||||||
|
// Find all controls with showWhen attribute
|
||||||
|
const conditionalControls = this.container.querySelectorAll('[data-show-when]');
|
||||||
|
|
||||||
|
conditionalControls.forEach(control => {
|
||||||
|
const triggerControlId = control.dataset.showWhen;
|
||||||
|
const triggerControl = document.getElementById(triggerControlId);
|
||||||
|
|
||||||
|
if (triggerControl) {
|
||||||
|
const updateVisibility = () => {
|
||||||
|
if (triggerControl.type === 'checkbox') {
|
||||||
|
control.style.display = triggerControl.checked ? 'block' : 'none';
|
||||||
|
} else if (triggerControl.tagName === 'SELECT') {
|
||||||
|
// Get the control ID to determine what value to check for
|
||||||
|
const controlId = control.querySelector('input, select')?.id;
|
||||||
|
|
||||||
|
// For background controls
|
||||||
|
if (triggerControlId === 'bg-type') {
|
||||||
|
if (controlId === 'bg-color') {
|
||||||
|
control.style.display = triggerControl.value === 'solid' ? 'block' : 'none';
|
||||||
|
} else if (controlId && (controlId.startsWith('gradient-') || controlId === 'gradient-angle')) {
|
||||||
|
control.style.display = triggerControl.value === 'gradient' ? 'block' : 'none';
|
||||||
|
} else if (controlId && (controlId.startsWith('texture-') || controlId === 'texture-type' || controlId === 'texture-scale')) {
|
||||||
|
control.style.display = triggerControl.value === 'texture' ? 'block' : 'none';
|
||||||
|
} else if (controlId && (controlId.startsWith('emoji-') || controlId === 'emoji-text')) {
|
||||||
|
control.style.display = triggerControl.value === 'emoji-wallpaper' ? 'block' : 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// For text color controls
|
||||||
|
else if (triggerControlId === 'text-color-type') {
|
||||||
|
if (controlId === 'text-color') {
|
||||||
|
control.style.display = triggerControl.value === 'solid' ? 'block' : 'none';
|
||||||
|
} else if (controlId && controlId.startsWith('text-gradient-')) {
|
||||||
|
control.style.display = triggerControl.value === 'gradient' ? 'block' : 'none';
|
||||||
|
}
|
||||||
|
} else if (triggerControlId === 'text2-color-type') {
|
||||||
|
if (controlId === 'text2-color') {
|
||||||
|
control.style.display = triggerControl.value === 'solid' ? 'block' : 'none';
|
||||||
|
} else if (controlId && controlId.startsWith('text2-gradient-')) {
|
||||||
|
control.style.display = triggerControl.value === 'gradient' ? 'block' : 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// For border style controls
|
||||||
|
else if (triggerControlId === 'border-style') {
|
||||||
|
if (controlId === 'border-rainbow-speed') {
|
||||||
|
control.style.display = triggerControl.value === 'rainbow' ? 'block' : 'none';
|
||||||
|
} else if (controlId === 'border-march-speed') {
|
||||||
|
control.style.display = triggerControl.value === 'marching-ants' ? 'block' : 'none';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Default: show when any value is selected
|
||||||
|
control.style.display = triggerControl.value ? 'block' : 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initial visibility
|
||||||
|
updateVisibility();
|
||||||
|
|
||||||
|
// Update on change
|
||||||
|
triggerControl.addEventListener('change', updateVisibility);
|
||||||
|
triggerControl.addEventListener('input', updateVisibility);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue