Start of the rewrite to a modular system

This commit is contained in:
Dan 2026-01-09 09:15:05 +00:00
parent 2bfdc30caa
commit 4ac45367e5
29 changed files with 4414 additions and 588 deletions

View file

@ -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>

View file

@ -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>

View 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>

View 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());
}
});
}
}

View 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 ===');
}

View 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);
}
}

View 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
*/

View 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());
}

View 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());
}

View 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());
}

View 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());
}

View 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());
}

View 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());
}

View file

@ -0,0 +1,139 @@
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: 'inset', label: 'Inset (3D)' },
{ value: 'outset', label: 'Outset (3D)' },
{ value: 'ridge', label: 'Ridge' }
]
}
];
}
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 === 'inset' || style === 'outset') {
this.draw3DBorder(context, width, style === 'outset', renderData);
} else if (style === 'ridge') {
this.drawRidgeBorder(context, width, 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, isOutset, renderData) {
const lightColor = isOutset ? '#ffffff' : '#000000';
const darkColor = isOutset ? '#000000' : '#ffffff';
// Top and left (light)
context.strokeStyle = lightColor;
context.lineWidth = width;
context.beginPath();
context.moveTo(0, renderData.height);
context.lineTo(0, 0);
context.lineTo(renderData.width, 0);
context.stroke();
// Bottom and right (dark)
context.strokeStyle = darkColor;
context.beginPath();
context.moveTo(renderData.width, 0);
context.lineTo(renderData.width, renderData.height);
context.lineTo(0, renderData.height);
context.stroke();
}
/**
* 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
);
}
}
// Auto-register effect
export function register(generator) {
generator.registerEffect(new BorderEffect());
}

View 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());
}

View 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());
}

View 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());
}

View 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());
}

View 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));
}

View 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());
}

View 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());
}

View 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());
}

View 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());
}

View file

@ -0,0 +1,144 @@
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: 0.1,
max: 5,
step: 0.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;
// 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 < text.length; i++) {
const char = text[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));
}

View file

@ -0,0 +1,209 @@
/**
* 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
*/

View file

@ -0,0 +1,232 @@
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: `text${suffix}-enabled`,
type: 'checkbox',
label: `Enable Text Line ${textLineNumber}`,
defaultValue: textLineNumber === 1
},
{
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}`];
const enabled = controlValues[`text${suffix}-enabled`];
// Only render if text exists, is enabled, and no animations are active on this text
const waveActive = controlValues[`animate-text-wave${suffix}`];
const rainbowActive = controlValues[`animate-text-rainbow${suffix}`];
return text && enabled && !waveActive && !rainbowActive;
}
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));
}

View file

@ -0,0 +1,167 @@
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);
// 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 < text.length; i++) {
const char = text[i];
const charWidth = context.measureText(char).width;
// Calculate wave offset for this character
const phase = animState.getPhase(speed);
const charOffset = i / text.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));
}

View file

@ -0,0 +1,352 @@
/**
* 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 border from "./effects/border.js";
import * as standardText from "./effects/text-standard.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: [
"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);
border.register(generator);
standardText.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();

View file

@ -0,0 +1,367 @@
/**
* UI Builder - Dynamically generates control UI from effect definitions
*/
export class UIBuilder {
constructor(containerElement) {
this.container = containerElement;
this.controlGroups = new Map(); // category -> { element, controls }
}
/**
* 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);
case 'range':
return this.createRange(id, label, defaultValue, min, max, step, description, showWhen);
case 'color':
return this.createColor(id, label, defaultValue, showWhen);
case 'select':
return this.createSelect(id, label, defaultValue, options, showWhen);
case 'text':
return this.createTextInput(id, label, defaultValue);
default:
console.warn(`Unknown control type: ${type}`);
return null;
}
}
/**
* Create a checkbox control
*/
createCheckbox(id, label, defaultValue, showWhen) {
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;
}
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>`;
if (description) {
labelEl.title = description;
}
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;
}
return container;
}
/**
* Create a color picker control
*/
createColor(id, label, defaultValue, showWhen) {
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;
}
return container;
}
/**
* Create a select dropdown control
*/
createSelect(id, label, defaultValue, options, showWhen) {
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;
}
return container;
}
/**
* Create a text input control
*/
createTextInput(id, label, defaultValue) {
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);
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';
}
} 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);
}
});
}
}