Merge branch 'main' into feature/terminal-game-engine
This commit is contained in:
commit
691b809b41
11 changed files with 1172 additions and 83 deletions
|
|
@ -1,4 +1,4 @@
|
||||||
@import url(https://fonts.bunny.net/css?family=bebas-neue:400|lato:400,400i,700,700i|montserrat:400,400i,700,700i|open-sans:400,400i,700,700i|oswald:400,700|press-start-2p:400|roboto:400,400i,700,700i|roboto-mono:400,400i,700,700i|vt323:400);
|
@import url(https://fonts.bunny.net/css?family=barrio:400|bungee-spice:400|creepster:400|pixelify-sans:400,700|bebas-neue:400|lato:400,400i,700,700i|montserrat:400,400i,700,700i|open-sans:400,400i,700,700i|oswald:400,700|press-start-2p:400|roboto:400,400i,700,700i|roboto-mono:400,400i,700,700i|vt323:400);
|
||||||
|
|
||||||
#button-generator-app {
|
#button-generator-app {
|
||||||
margin: 2rem 0;
|
margin: 2rem 0;
|
||||||
|
|
@ -324,7 +324,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Custom file input styling
|
// Custom file input styling
|
||||||
input[type="file"] {
|
input[type="file"] {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ Big thanks to [neonaut's 88x31 archive](https://neonaut.neocities.org/cyber/88x3
|
||||||
- 08/01/2025 - Total refactor to be modular, added many more effects.
|
- 08/01/2025 - Total refactor to be modular, added many more effects.
|
||||||
- 09/01/2025 - Rewrote the bevel inset and outset border code so they look a lot nicer at the corners. Updates throughout so that multibyte (emoji!) characters should work.
|
- 09/01/2025 - Rewrote the bevel inset and outset border code so they look a lot nicer at the corners. Updates throughout so that multibyte (emoji!) characters should work.
|
||||||
- 13/01/2025 - Added ticker scrolling and flashing text options. Added a background image selector.
|
- 13/01/2025 - Added ticker scrolling and flashing text options. Added a background image selector.
|
||||||
|
- 22/01/2025 - Added a few new effects and multiple new fonts. Send font requests if you want something specific!
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
10
content/updates/2026-01-22-button-generator-update.md
Normal file
10
content/updates/2026-01-22-button-generator-update.md
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
---
|
||||||
|
title: "2026 01 22 Button Generator Update"
|
||||||
|
date: 2026-01-22T09:59:07Z
|
||||||
|
tags: []
|
||||||
|
description: ""
|
||||||
|
build:
|
||||||
|
render: never
|
||||||
|
---
|
||||||
|
|
||||||
|
Added new effects and fonts to the 88x31 button generator
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { ButtonEffect } from './effect-base.js';
|
import { ButtonEffect } from "./effect-base.js";
|
||||||
|
|
||||||
import { ColorQuantizer } from './color-quantizer.js';
|
import { ColorQuantizer } from "./color-quantizer.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Animation state class - passed to effects for frame-based rendering
|
* Animation state class - passed to effects for frame-based rendering
|
||||||
|
|
@ -30,7 +30,7 @@ export class AnimationState {
|
||||||
export class ButtonGenerator {
|
export class ButtonGenerator {
|
||||||
constructor(canvas, config = {}) {
|
constructor(canvas, config = {}) {
|
||||||
this.canvas = canvas;
|
this.canvas = canvas;
|
||||||
this.ctx = canvas.getContext('2d');
|
this.ctx = canvas.getContext("2d");
|
||||||
|
|
||||||
// Animation configuration
|
// Animation configuration
|
||||||
this.animConfig = {
|
this.animConfig = {
|
||||||
|
|
@ -38,7 +38,7 @@ export class ButtonGenerator {
|
||||||
duration: config.duration || 2, // seconds
|
duration: config.duration || 2, // seconds
|
||||||
get totalFrames() {
|
get totalFrames() {
|
||||||
return this.fps * this.duration;
|
return this.fps * this.duration;
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// GIF export configuration
|
// GIF export configuration
|
||||||
|
|
@ -55,7 +55,7 @@ export class ButtonGenerator {
|
||||||
border: [],
|
border: [],
|
||||||
text: [],
|
text: [],
|
||||||
text2: [],
|
text2: [],
|
||||||
general: []
|
general: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
// Registered effects by ID for quick lookup
|
// Registered effects by ID for quick lookup
|
||||||
|
|
@ -69,8 +69,20 @@ export class ButtonGenerator {
|
||||||
|
|
||||||
// Font list for preloading
|
// Font list for preloading
|
||||||
this.fonts = config.fonts || [
|
this.fonts = config.fonts || [
|
||||||
'Lato', 'Roboto', 'Open Sans', 'Montserrat', 'Oswald',
|
"Lato",
|
||||||
'Bebas Neue', 'Roboto Mono', 'VT323', 'Press Start 2P', 'DSEG7-Classic'
|
"Roboto",
|
||||||
|
"Open Sans",
|
||||||
|
"Montserrat",
|
||||||
|
"Oswald",
|
||||||
|
"Bebas Neue",
|
||||||
|
"Roboto Mono",
|
||||||
|
"VT323",
|
||||||
|
"Press Start 2P",
|
||||||
|
"DSEG7-Classic",
|
||||||
|
"Pixelify Sans",
|
||||||
|
"Bungee Spice",
|
||||||
|
"Creepster",
|
||||||
|
"Barrio",
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -80,11 +92,13 @@ export class ButtonGenerator {
|
||||||
*/
|
*/
|
||||||
registerEffect(effect) {
|
registerEffect(effect) {
|
||||||
if (!(effect instanceof ButtonEffect)) {
|
if (!(effect instanceof ButtonEffect)) {
|
||||||
throw new Error('Effect must extend ButtonEffect class');
|
throw new Error("Effect must extend ButtonEffect class");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.effectsById.has(effect.id)) {
|
if (this.effectsById.has(effect.id)) {
|
||||||
console.warn(`Effect with ID "${effect.id}" is already registered. Skipping.`);
|
console.warn(
|
||||||
|
`Effect with ID "${effect.id}" is already registered. Skipping.`,
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -125,14 +139,14 @@ export class ButtonGenerator {
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
async preloadFonts() {
|
async preloadFonts() {
|
||||||
const fontPromises = this.fonts.flatMap(font => [
|
const fontPromises = this.fonts.flatMap((font) => [
|
||||||
document.fonts.load(`400 12px "${font}"`),
|
document.fonts.load(`400 12px "${font}"`),
|
||||||
document.fonts.load(`700 12px "${font}"`),
|
document.fonts.load(`700 12px "${font}"`),
|
||||||
document.fonts.load(`italic 400 12px "${font}"`)
|
document.fonts.load(`italic 400 12px "${font}"`),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
await Promise.all(fontPromises);
|
await Promise.all(fontPromises);
|
||||||
console.log('All fonts loaded for canvas');
|
console.log("All fonts loaded for canvas");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -144,28 +158,28 @@ export class ButtonGenerator {
|
||||||
|
|
||||||
// Get all registered control IDs from effects
|
// Get all registered control IDs from effects
|
||||||
const allControls = new Set();
|
const allControls = new Set();
|
||||||
this.getAllEffects().forEach(effect => {
|
this.getAllEffects().forEach((effect) => {
|
||||||
effect.controls.forEach(control => {
|
effect.controls.forEach((control) => {
|
||||||
allControls.add(control.id);
|
allControls.add(control.id);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Read values from DOM
|
// Read values from DOM
|
||||||
allControls.forEach(id => {
|
allControls.forEach((id) => {
|
||||||
const element = document.getElementById(id);
|
const element = document.getElementById(id);
|
||||||
if (element) {
|
if (element) {
|
||||||
if (element.type === 'checkbox') {
|
if (element.type === "checkbox") {
|
||||||
values[id] = element.checked;
|
values[id] = element.checked;
|
||||||
} else if (element.type === 'range' || element.type === 'number') {
|
} else if (element.type === "range" || element.type === "number") {
|
||||||
values[id] = parseFloat(element.value);
|
values[id] = parseFloat(element.value);
|
||||||
} else if (element.type === 'file') {
|
} else if (element.type === "file") {
|
||||||
// For file inputs, return an object with file metadata and blob URL
|
// For file inputs, return an object with file metadata and blob URL
|
||||||
if (element.dataset.blobUrl) {
|
if (element.dataset.blobUrl) {
|
||||||
values[id] = {
|
values[id] = {
|
||||||
fileName: element.dataset.fileName,
|
fileName: element.dataset.fileName,
|
||||||
blobUrl: element.dataset.blobUrl,
|
blobUrl: element.dataset.blobUrl,
|
||||||
fileSize: parseInt(element.dataset.fileSize),
|
fileSize: parseInt(element.dataset.fileSize),
|
||||||
fileType: element.dataset.fileType
|
fileType: element.dataset.fileType,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
values[id] = null;
|
values[id] = null;
|
||||||
|
|
@ -200,21 +214,29 @@ export class ButtonGenerator {
|
||||||
width: this.canvas.width,
|
width: this.canvas.width,
|
||||||
height: this.canvas.height,
|
height: this.canvas.height,
|
||||||
centerX: this.canvas.width / 2,
|
centerX: this.canvas.width / 2,
|
||||||
centerY: this.canvas.height / 2
|
centerY: this.canvas.height / 2,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Apply effects in order: transform -> background -> background-animation -> text/text2 -> border -> general
|
// Apply effects in order: transform -> background -> background-animation -> text/text2 -> border -> general
|
||||||
const renderOrder = ['transform', 'background', 'background-animation', 'text', 'text2', 'border', 'general'];
|
const renderOrder = [
|
||||||
|
"transform",
|
||||||
|
"background",
|
||||||
|
"background-animation",
|
||||||
|
"text",
|
||||||
|
"text2",
|
||||||
|
"border",
|
||||||
|
"general",
|
||||||
|
];
|
||||||
|
|
||||||
// Save context once before transforms
|
// Save context once before transforms
|
||||||
this.ctx.save();
|
this.ctx.save();
|
||||||
|
|
||||||
renderOrder.forEach(type => {
|
renderOrder.forEach((type) => {
|
||||||
this.effects[type]?.forEach(effect => {
|
this.effects[type]?.forEach((effect) => {
|
||||||
if (effect.canApply(controlValues)) {
|
if (effect.canApply(controlValues)) {
|
||||||
// Transform effects should NOT be wrapped in save/restore
|
// Transform effects should NOT be wrapped in save/restore
|
||||||
// They need to persist for all subsequent drawing operations
|
// They need to persist for all subsequent drawing operations
|
||||||
if (type !== 'transform') {
|
if (type !== "transform") {
|
||||||
this.ctx.save();
|
this.ctx.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -224,7 +246,7 @@ export class ButtonGenerator {
|
||||||
console.error(`Error applying effect ${effect.id}:`, error);
|
console.error(`Error applying effect ${effect.id}:`, error);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type !== 'transform') {
|
if (type !== "transform") {
|
||||||
this.ctx.restore();
|
this.ctx.restore();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -241,9 +263,10 @@ export class ButtonGenerator {
|
||||||
*/
|
*/
|
||||||
hasAnimationsEnabled() {
|
hasAnimationsEnabled() {
|
||||||
const controlValues = this.getControlValues();
|
const controlValues = this.getControlValues();
|
||||||
return this.getAllEffects().some(effect =>
|
return this.getAllEffects().some(
|
||||||
effect.type !== 'background' && // Background effects can be static
|
(effect) =>
|
||||||
effect.isEnabled(controlValues)
|
effect.type !== "background" && // Background effects can be static
|
||||||
|
effect.isEnabled(controlValues),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -264,7 +287,7 @@ export class ButtonGenerator {
|
||||||
const animState = new AnimationState(
|
const animState = new AnimationState(
|
||||||
frameNum,
|
frameNum,
|
||||||
this.animConfig.totalFrames,
|
this.animConfig.totalFrames,
|
||||||
this.animConfig.fps
|
this.animConfig.fps,
|
||||||
);
|
);
|
||||||
this.draw(animState);
|
this.draw(animState);
|
||||||
this.applyPreviewQuantization();
|
this.applyPreviewQuantization();
|
||||||
|
|
@ -308,7 +331,11 @@ export class ButtonGenerator {
|
||||||
applyPreviewQuantization() {
|
applyPreviewQuantization() {
|
||||||
const colorCount = this.gifConfig.colorCount;
|
const colorCount = this.gifConfig.colorCount;
|
||||||
if (colorCount < 256) {
|
if (colorCount < 256) {
|
||||||
const quantizedData = ColorQuantizer.quantize(this.canvas, colorCount, 'floyd-steinberg');
|
const quantizedData = ColorQuantizer.quantize(
|
||||||
|
this.canvas,
|
||||||
|
colorCount,
|
||||||
|
"floyd-steinberg",
|
||||||
|
);
|
||||||
this.ctx.putImageData(quantizedData, 0, 0);
|
this.ctx.putImageData(quantizedData, 0, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -326,27 +353,34 @@ export class ButtonGenerator {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
try {
|
try {
|
||||||
// Create temporary canvas for frame generation
|
// Create temporary canvas for frame generation
|
||||||
const frameCanvas = document.createElement('canvas');
|
const frameCanvas = document.createElement("canvas");
|
||||||
frameCanvas.width = this.canvas.width;
|
frameCanvas.width = this.canvas.width;
|
||||||
frameCanvas.height = this.canvas.height;
|
frameCanvas.height = this.canvas.height;
|
||||||
const frameCtx = frameCanvas.getContext('2d');
|
const frameCtx = frameCanvas.getContext("2d");
|
||||||
|
|
||||||
// Merge options with defaults
|
// Merge options with defaults
|
||||||
const quality = options.quality !== undefined ? options.quality : this.gifConfig.quality;
|
const quality =
|
||||||
const gifDither = options.dither !== undefined ? options.dither : this.gifConfig.dither;
|
options.quality !== undefined
|
||||||
const colorCount = options.colorCount !== undefined ? options.colorCount : this.gifConfig.colorCount;
|
? options.quality
|
||||||
|
: this.gifConfig.quality;
|
||||||
|
const gifDither =
|
||||||
|
options.dither !== undefined ? options.dither : this.gifConfig.dither;
|
||||||
|
const colorCount =
|
||||||
|
options.colorCount !== undefined
|
||||||
|
? options.colorCount
|
||||||
|
: this.gifConfig.colorCount;
|
||||||
|
|
||||||
// Determine if we need custom quantization
|
// Determine if we need custom quantization
|
||||||
const useCustomQuantization = colorCount < 256;
|
const useCustomQuantization = colorCount < 256;
|
||||||
const customDither = useCustomQuantization ? 'floyd-steinberg' : false;
|
const customDither = useCustomQuantization ? "floyd-steinberg" : false;
|
||||||
|
|
||||||
// Initialize gif.js
|
// Initialize gif.js
|
||||||
const gifOptions = {
|
const gifOptions = {
|
||||||
workers: 2,
|
workers: 2,
|
||||||
quality: quality,
|
quality: quality,
|
||||||
workerScript: '/js/gif.worker.js',
|
workerScript: "/js/gif.worker.js",
|
||||||
width: this.canvas.width,
|
width: this.canvas.width,
|
||||||
height: this.canvas.height
|
height: this.canvas.height,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add gif.js dither option if specified (only when not using custom quantization)
|
// Add gif.js dither option if specified (only when not using custom quantization)
|
||||||
|
|
@ -361,18 +395,22 @@ export class ButtonGenerator {
|
||||||
|
|
||||||
const generateFrames = async () => {
|
const generateFrames = async () => {
|
||||||
for (let i = 0; i < totalFrames; i++) {
|
for (let i = 0; i < totalFrames; i++) {
|
||||||
const animState = new AnimationState(i, totalFrames, this.animConfig.fps);
|
const animState = new AnimationState(
|
||||||
|
i,
|
||||||
|
totalFrames,
|
||||||
|
this.animConfig.fps,
|
||||||
|
);
|
||||||
|
|
||||||
// Draw to temporary canvas
|
// Draw to temporary canvas
|
||||||
frameCtx.clearRect(0, 0, frameCanvas.width, frameCanvas.height);
|
frameCtx.clearRect(0, 0, frameCanvas.width, frameCanvas.height);
|
||||||
const tempGenerator = new ButtonGenerator(frameCanvas, {
|
const tempGenerator = new ButtonGenerator(frameCanvas, {
|
||||||
fps: this.animConfig.fps,
|
fps: this.animConfig.fps,
|
||||||
duration: this.animConfig.duration,
|
duration: this.animConfig.duration,
|
||||||
fonts: this.fonts
|
fonts: this.fonts,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Copy effects
|
// Copy effects
|
||||||
this.getAllEffects().forEach(effect => {
|
this.getAllEffects().forEach((effect) => {
|
||||||
tempGenerator.registerEffect(effect);
|
tempGenerator.registerEffect(effect);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -380,40 +418,43 @@ export class ButtonGenerator {
|
||||||
|
|
||||||
// Apply custom color quantization if needed
|
// Apply custom color quantization if needed
|
||||||
if (useCustomQuantization) {
|
if (useCustomQuantization) {
|
||||||
const quantizedData = ColorQuantizer.quantize(frameCanvas, colorCount, customDither);
|
const quantizedData = ColorQuantizer.quantize(
|
||||||
|
frameCanvas,
|
||||||
|
colorCount,
|
||||||
|
customDither,
|
||||||
|
);
|
||||||
frameCtx.putImageData(quantizedData, 0, 0);
|
frameCtx.putImageData(quantizedData, 0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
gif.addFrame(frameCtx, {
|
gif.addFrame(frameCtx, {
|
||||||
delay: 1000 / this.animConfig.fps,
|
delay: 1000 / this.animConfig.fps,
|
||||||
copy: true
|
copy: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (progressCallback) {
|
if (progressCallback) {
|
||||||
progressCallback(i / totalFrames, 'generating');
|
progressCallback(i / totalFrames, "generating");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Yield to browser every 5 frames
|
// Yield to browser every 5 frames
|
||||||
if (i % 5 === 0) {
|
if (i % 5 === 0) {
|
||||||
await new Promise(resolve => setTimeout(resolve, 0));
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
generateFrames().then(() => {
|
generateFrames().then(() => {
|
||||||
gif.on('finished', (blob) => {
|
gif.on("finished", (blob) => {
|
||||||
resolve(blob);
|
resolve(blob);
|
||||||
});
|
});
|
||||||
|
|
||||||
gif.on('progress', (progress) => {
|
gif.on("progress", (progress) => {
|
||||||
if (progressCallback) {
|
if (progressCallback) {
|
||||||
progressCallback(progress, 'encoding');
|
progressCallback(progress, "encoding");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
gif.render();
|
gif.render();
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
reject(error);
|
reject(error);
|
||||||
}
|
}
|
||||||
|
|
@ -425,17 +466,17 @@ export class ButtonGenerator {
|
||||||
*/
|
*/
|
||||||
bindControls() {
|
bindControls() {
|
||||||
const allControls = new Set();
|
const allControls = new Set();
|
||||||
this.getAllEffects().forEach(effect => {
|
this.getAllEffects().forEach((effect) => {
|
||||||
effect.controls.forEach(control => {
|
effect.controls.forEach((control) => {
|
||||||
allControls.add(control.id);
|
allControls.add(control.id);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
allControls.forEach(id => {
|
allControls.forEach((id) => {
|
||||||
const element = document.getElementById(id);
|
const element = document.getElementById(id);
|
||||||
if (element) {
|
if (element) {
|
||||||
element.addEventListener('input', () => this.updatePreview());
|
element.addEventListener("input", () => this.updatePreview());
|
||||||
element.addEventListener('change', () => this.updatePreview());
|
element.addEventListener("change", () => this.updatePreview());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
208
static/js/button-generator/effects/background-plasma.js
Normal file
208
static/js/button-generator/effects/background-plasma.js
Normal file
|
|
@ -0,0 +1,208 @@
|
||||||
|
import { ButtonEffect } from "../effect-base.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plasma background effect
|
||||||
|
* Classic demoscene sine wave plasma - iconic 90s visual
|
||||||
|
*/
|
||||||
|
export class PlasmaEffect extends ButtonEffect {
|
||||||
|
constructor() {
|
||||||
|
super({
|
||||||
|
id: "bg-plasma",
|
||||||
|
name: "Plasma",
|
||||||
|
type: "background-animation",
|
||||||
|
category: "Background Animations",
|
||||||
|
renderOrder: 5,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
defineControls() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: "animate-plasma",
|
||||||
|
type: "checkbox",
|
||||||
|
label: "Plasma Effect",
|
||||||
|
defaultValue: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "plasma-scale",
|
||||||
|
type: "range",
|
||||||
|
label: "Pattern Scale",
|
||||||
|
defaultValue: 1.5,
|
||||||
|
min: 0.5,
|
||||||
|
max: 3,
|
||||||
|
step: 0.1,
|
||||||
|
showWhen: "animate-plasma",
|
||||||
|
description: "Size of plasma patterns",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "plasma-speed",
|
||||||
|
type: "range",
|
||||||
|
label: "Animation Speed",
|
||||||
|
defaultValue: 1,
|
||||||
|
min: 0.5,
|
||||||
|
max: 3,
|
||||||
|
step: 0.1,
|
||||||
|
showWhen: "animate-plasma",
|
||||||
|
description: "Speed of plasma movement",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "plasma-color-scheme",
|
||||||
|
type: "select",
|
||||||
|
label: "Color Scheme",
|
||||||
|
defaultValue: "classic",
|
||||||
|
options: [
|
||||||
|
{ value: "classic", label: "Classic (Purple/Cyan)" },
|
||||||
|
{ value: "fire", label: "Fire (Red/Orange)" },
|
||||||
|
{ value: "ocean", label: "Ocean (Blue/Green)" },
|
||||||
|
{ value: "psychedelic", label: "Psychedelic (Rainbow)" },
|
||||||
|
{ value: "matrix", label: "Matrix (Green)" },
|
||||||
|
],
|
||||||
|
showWhen: "animate-plasma",
|
||||||
|
description: "Color palette for plasma",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
isEnabled(controlValues) {
|
||||||
|
return controlValues["animate-plasma"] === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get color based on scheme and value
|
||||||
|
*/
|
||||||
|
getColor(value, scheme) {
|
||||||
|
// Normalize value from -4..4 to 0..1
|
||||||
|
const normalized = (value + 4) / 8;
|
||||||
|
|
||||||
|
switch (scheme) {
|
||||||
|
case "fire": {
|
||||||
|
// Black -> Red -> Orange -> Yellow -> White
|
||||||
|
const r = Math.min(255, normalized * 400);
|
||||||
|
const g = Math.max(0, Math.min(255, (normalized - 0.3) * 400));
|
||||||
|
const b = Math.max(0, Math.min(255, (normalized - 0.7) * 400));
|
||||||
|
return { r, g, b };
|
||||||
|
}
|
||||||
|
|
||||||
|
case "ocean": {
|
||||||
|
// Deep blue -> Cyan -> Light green
|
||||||
|
const r = Math.max(0, Math.min(100, normalized * 100));
|
||||||
|
const g = Math.min(255, normalized * 300);
|
||||||
|
const b = Math.min(255, 100 + normalized * 155);
|
||||||
|
return { r, g, b };
|
||||||
|
}
|
||||||
|
|
||||||
|
case "psychedelic": {
|
||||||
|
// Full rainbow cycling
|
||||||
|
const hue = normalized * 360;
|
||||||
|
return this.hslToRgb(hue, 100, 50);
|
||||||
|
}
|
||||||
|
|
||||||
|
case "matrix": {
|
||||||
|
// Black -> Dark green -> Bright green -> White
|
||||||
|
const intensity = normalized;
|
||||||
|
const r = Math.max(0, Math.min(255, (intensity - 0.8) * 1000));
|
||||||
|
const g = Math.min(255, intensity * 300);
|
||||||
|
const b = Math.max(0, Math.min(255, (intensity - 0.9) * 500));
|
||||||
|
return { r, g, b };
|
||||||
|
}
|
||||||
|
|
||||||
|
case "classic":
|
||||||
|
default: {
|
||||||
|
// Classic demoscene: purple/magenta/cyan
|
||||||
|
const r = Math.min(255, 128 + Math.sin(normalized * Math.PI * 2) * 127);
|
||||||
|
const g = Math.min(255, 64 + Math.sin(normalized * Math.PI * 2 + 2) * 64);
|
||||||
|
const b = Math.min(255, 128 + Math.sin(normalized * Math.PI * 2 + 4) * 127);
|
||||||
|
return { r, g, b };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert HSL to RGB
|
||||||
|
*/
|
||||||
|
hslToRgb(h, s, l) {
|
||||||
|
s /= 100;
|
||||||
|
l /= 100;
|
||||||
|
const c = (1 - Math.abs(2 * l - 1)) * s;
|
||||||
|
const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
|
||||||
|
const m = l - c / 2;
|
||||||
|
|
||||||
|
let r, g, b;
|
||||||
|
if (h < 60) {
|
||||||
|
r = c; g = x; b = 0;
|
||||||
|
} else if (h < 120) {
|
||||||
|
r = x; g = c; b = 0;
|
||||||
|
} else if (h < 180) {
|
||||||
|
r = 0; g = c; b = x;
|
||||||
|
} else if (h < 240) {
|
||||||
|
r = 0; g = x; b = c;
|
||||||
|
} else if (h < 300) {
|
||||||
|
r = x; g = 0; b = c;
|
||||||
|
} else {
|
||||||
|
r = c; g = 0; b = x;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
r: Math.round((r + m) * 255),
|
||||||
|
g: Math.round((g + m) * 255),
|
||||||
|
b: Math.round((b + m) * 255),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
apply(context, controlValues, animState, renderData) {
|
||||||
|
if (!animState) return;
|
||||||
|
|
||||||
|
const scale = controlValues["plasma-scale"] || 1.5;
|
||||||
|
const speed = controlValues["plasma-speed"] || 1;
|
||||||
|
const scheme = controlValues["plasma-color-scheme"] || "classic";
|
||||||
|
|
||||||
|
const { width, height } = renderData;
|
||||||
|
|
||||||
|
// Perfect looping via ping-pong: animate forward first half, reverse second half
|
||||||
|
// progress goes 0->1, we convert to 0->1->0 (triangle wave)
|
||||||
|
const progress = animState.progress;
|
||||||
|
let pingPong;
|
||||||
|
if (progress < 0.5) {
|
||||||
|
// First half: 0->1
|
||||||
|
pingPong = progress * 2;
|
||||||
|
} else {
|
||||||
|
// Second half: 1->0
|
||||||
|
pingPong = (1 - progress) * 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scale the time value for visible animation
|
||||||
|
const time = pingPong * Math.PI * 2 * speed;
|
||||||
|
|
||||||
|
// Get image data for pixel manipulation
|
||||||
|
const imageData = context.getImageData(0, 0, width, height);
|
||||||
|
const data = imageData.data;
|
||||||
|
|
||||||
|
// Scale factor for the plasma pattern (smaller = larger patterns)
|
||||||
|
const scaleFactor = 10 / scale;
|
||||||
|
|
||||||
|
for (let y = 0; y < height; y++) {
|
||||||
|
for (let x = 0; x < width; x++) {
|
||||||
|
// Classic plasma formula
|
||||||
|
const value =
|
||||||
|
Math.sin(x / scaleFactor + time) +
|
||||||
|
Math.sin(y / scaleFactor + time * 1.3) +
|
||||||
|
Math.sin((x + y) / scaleFactor + time * 0.7) +
|
||||||
|
Math.sin(Math.sqrt(x * x + y * y) / scaleFactor + time);
|
||||||
|
|
||||||
|
const color = this.getColor(value, scheme);
|
||||||
|
|
||||||
|
const idx = (y * width + x) * 4;
|
||||||
|
data[idx] = color.r;
|
||||||
|
data[idx + 1] = color.g;
|
||||||
|
data[idx + 2] = color.b;
|
||||||
|
data[idx + 3] = 255;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
context.putImageData(imageData, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function register(generator) {
|
||||||
|
generator.registerEffect(new PlasmaEffect());
|
||||||
|
}
|
||||||
168
static/js/button-generator/effects/background-sparkle.js
Normal file
168
static/js/button-generator/effects/background-sparkle.js
Normal file
|
|
@ -0,0 +1,168 @@
|
||||||
|
import { ButtonEffect } from "../effect-base.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sparkle/Twinkle background effect
|
||||||
|
* Random twinkling stars overlay - classic 88x31 button aesthetic
|
||||||
|
*/
|
||||||
|
export class SparkleEffect extends ButtonEffect {
|
||||||
|
constructor() {
|
||||||
|
super({
|
||||||
|
id: "bg-sparkle",
|
||||||
|
name: "Sparkle",
|
||||||
|
type: "background-animation",
|
||||||
|
category: "Background Animations",
|
||||||
|
renderOrder: 15,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.sparkles = [];
|
||||||
|
this.initialized = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
defineControls() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: "animate-sparkle",
|
||||||
|
type: "checkbox",
|
||||||
|
label: "Sparkle Effect",
|
||||||
|
defaultValue: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "sparkle-density",
|
||||||
|
type: "range",
|
||||||
|
label: "Sparkle Count",
|
||||||
|
defaultValue: 20,
|
||||||
|
min: 5,
|
||||||
|
max: 50,
|
||||||
|
step: 1,
|
||||||
|
showWhen: "animate-sparkle",
|
||||||
|
description: "Number of sparkles",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "sparkle-size",
|
||||||
|
type: "range",
|
||||||
|
label: "Max Size",
|
||||||
|
defaultValue: 3,
|
||||||
|
min: 1,
|
||||||
|
max: 5,
|
||||||
|
step: 0.5,
|
||||||
|
showWhen: "animate-sparkle",
|
||||||
|
description: "Maximum sparkle size",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "sparkle-speed",
|
||||||
|
type: "range",
|
||||||
|
label: "Twinkle Speed",
|
||||||
|
defaultValue: 1.5,
|
||||||
|
min: 0.5,
|
||||||
|
max: 3,
|
||||||
|
step: 0.1,
|
||||||
|
showWhen: "animate-sparkle",
|
||||||
|
description: "How fast sparkles twinkle",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "sparkle-color",
|
||||||
|
type: "color",
|
||||||
|
label: "Sparkle Color",
|
||||||
|
defaultValue: "#ffffff",
|
||||||
|
showWhen: "animate-sparkle",
|
||||||
|
description: "Color of sparkles",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
isEnabled(controlValues) {
|
||||||
|
return controlValues["animate-sparkle"] === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
apply(context, controlValues, animState, renderData) {
|
||||||
|
if (!animState) return;
|
||||||
|
|
||||||
|
const density = controlValues["sparkle-density"] || 20;
|
||||||
|
const maxSize = controlValues["sparkle-size"] || 3;
|
||||||
|
const speed = controlValues["sparkle-speed"] || 1.5;
|
||||||
|
const sparkleColor = controlValues["sparkle-color"] || "#ffffff";
|
||||||
|
|
||||||
|
// Initialize sparkles on first frame or density change
|
||||||
|
if (!this.initialized || this.sparkles.length !== density) {
|
||||||
|
this.sparkles = [];
|
||||||
|
for (let i = 0; i < density; i++) {
|
||||||
|
this.sparkles.push({
|
||||||
|
x: Math.random() * renderData.width,
|
||||||
|
y: Math.random() * renderData.height,
|
||||||
|
phase: Math.random() * Math.PI * 2,
|
||||||
|
size: 1 + Math.random() * (maxSize - 1),
|
||||||
|
speedMult: 0.7 + Math.random() * 0.6,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.initialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse color
|
||||||
|
const hexToRgb = (hex) => {
|
||||||
|
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
||||||
|
return result
|
||||||
|
? {
|
||||||
|
r: parseInt(result[1], 16),
|
||||||
|
g: parseInt(result[2], 16),
|
||||||
|
b: parseInt(result[3], 16),
|
||||||
|
}
|
||||||
|
: { r: 255, g: 255, b: 255 };
|
||||||
|
};
|
||||||
|
|
||||||
|
const rgb = hexToRgb(sparkleColor);
|
||||||
|
|
||||||
|
// Draw sparkles
|
||||||
|
this.sparkles.forEach((sparkle) => {
|
||||||
|
// Calculate twinkle phase - each sparkle has its own timing
|
||||||
|
const phase = animState.getPhase(speed * sparkle.speedMult) + sparkle.phase;
|
||||||
|
|
||||||
|
// Use sin² for smooth fade in/out, with some sparkles fully off
|
||||||
|
const rawAlpha = Math.sin(phase);
|
||||||
|
const alpha = rawAlpha > 0 ? rawAlpha * rawAlpha : 0;
|
||||||
|
|
||||||
|
if (alpha < 0.05) return; // Skip nearly invisible sparkles
|
||||||
|
|
||||||
|
const size = sparkle.size * (0.5 + alpha * 0.5);
|
||||||
|
|
||||||
|
// Draw 4-point star shape
|
||||||
|
context.save();
|
||||||
|
context.translate(sparkle.x, sparkle.y);
|
||||||
|
context.fillStyle = `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${alpha})`;
|
||||||
|
|
||||||
|
// Draw diamond/star shape
|
||||||
|
context.beginPath();
|
||||||
|
|
||||||
|
// Horizontal points
|
||||||
|
context.moveTo(-size, 0);
|
||||||
|
context.lineTo(0, -size * 0.3);
|
||||||
|
context.lineTo(size, 0);
|
||||||
|
context.lineTo(0, size * 0.3);
|
||||||
|
context.closePath();
|
||||||
|
context.fill();
|
||||||
|
|
||||||
|
// Vertical points
|
||||||
|
context.beginPath();
|
||||||
|
context.moveTo(0, -size);
|
||||||
|
context.lineTo(size * 0.3, 0);
|
||||||
|
context.lineTo(0, size);
|
||||||
|
context.lineTo(-size * 0.3, 0);
|
||||||
|
context.closePath();
|
||||||
|
context.fill();
|
||||||
|
|
||||||
|
// Center glow
|
||||||
|
const gradient = context.createRadialGradient(0, 0, 0, 0, 0, size * 0.5);
|
||||||
|
gradient.addColorStop(0, `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${alpha})`);
|
||||||
|
gradient.addColorStop(1, `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0)`);
|
||||||
|
context.fillStyle = gradient;
|
||||||
|
context.beginPath();
|
||||||
|
context.arc(0, 0, size * 0.5, 0, Math.PI * 2);
|
||||||
|
context.fill();
|
||||||
|
|
||||||
|
context.restore();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function register(generator) {
|
||||||
|
generator.registerEffect(new SparkleEffect());
|
||||||
|
}
|
||||||
245
static/js/button-generator/effects/emboss.js
Normal file
245
static/js/button-generator/effects/emboss.js
Normal file
|
|
@ -0,0 +1,245 @@
|
||||||
|
import { ButtonEffect } from "../effect-base.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emboss/Bevel border effect
|
||||||
|
* Classic Windows 95/98 raised button appearance
|
||||||
|
*/
|
||||||
|
export class EmbossEffect extends ButtonEffect {
|
||||||
|
constructor() {
|
||||||
|
super({
|
||||||
|
id: "emboss",
|
||||||
|
name: "Emboss/Bevel",
|
||||||
|
type: "general",
|
||||||
|
category: "Visual Effects",
|
||||||
|
renderOrder: 95, // After everything, draws on top
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
defineControls() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: "enable-emboss",
|
||||||
|
type: "checkbox",
|
||||||
|
label: "Emboss/Bevel Effect",
|
||||||
|
defaultValue: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "emboss-style",
|
||||||
|
type: "select",
|
||||||
|
label: "Style",
|
||||||
|
defaultValue: "raised",
|
||||||
|
options: [
|
||||||
|
{ value: "raised", label: "Raised (Outset)" },
|
||||||
|
{ value: "sunken", label: "Sunken (Inset)" },
|
||||||
|
{ value: "ridge", label: "Ridge" },
|
||||||
|
{ value: "groove", label: "Groove" },
|
||||||
|
],
|
||||||
|
showWhen: "enable-emboss",
|
||||||
|
description: "Type of bevel effect",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "emboss-depth",
|
||||||
|
type: "range",
|
||||||
|
label: "Depth",
|
||||||
|
defaultValue: 2,
|
||||||
|
min: 1,
|
||||||
|
max: 4,
|
||||||
|
step: 1,
|
||||||
|
showWhen: "enable-emboss",
|
||||||
|
description: "Thickness of bevel in pixels",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "emboss-light-color",
|
||||||
|
type: "color",
|
||||||
|
label: "Highlight Color",
|
||||||
|
defaultValue: "#ffffff",
|
||||||
|
showWhen: "enable-emboss",
|
||||||
|
description: "Color for lit edges",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "emboss-shadow-color",
|
||||||
|
type: "color",
|
||||||
|
label: "Shadow Color",
|
||||||
|
defaultValue: "#000000",
|
||||||
|
showWhen: "enable-emboss",
|
||||||
|
description: "Color for shadowed edges",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
isEnabled(controlValues) {
|
||||||
|
return controlValues["enable-emboss"] === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse hex color to rgba string with alpha
|
||||||
|
*/
|
||||||
|
colorWithAlpha(hex, alpha) {
|
||||||
|
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
||||||
|
if (result) {
|
||||||
|
const r = parseInt(result[1], 16);
|
||||||
|
const g = parseInt(result[2], 16);
|
||||||
|
const b = parseInt(result[3], 16);
|
||||||
|
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
||||||
|
}
|
||||||
|
return `rgba(255, 255, 255, ${alpha})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw a single bevel layer
|
||||||
|
*/
|
||||||
|
drawBevel(
|
||||||
|
context,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
offset,
|
||||||
|
lightColor,
|
||||||
|
shadowColor,
|
||||||
|
lightAlpha,
|
||||||
|
shadowAlpha,
|
||||||
|
) {
|
||||||
|
// Top edge (light)
|
||||||
|
context.fillStyle = this.colorWithAlpha(lightColor, lightAlpha);
|
||||||
|
context.fillRect(offset, offset, width - offset * 2, 1);
|
||||||
|
|
||||||
|
// Left edge (light)
|
||||||
|
context.fillRect(offset, offset, 1, height - offset * 2);
|
||||||
|
|
||||||
|
// Bottom edge (shadow)
|
||||||
|
context.fillStyle = this.colorWithAlpha(shadowColor, shadowAlpha);
|
||||||
|
context.fillRect(offset, height - 1 - offset, width - offset * 2, 1);
|
||||||
|
|
||||||
|
// Right edge (shadow)
|
||||||
|
context.fillRect(width - 1 - offset, offset, 1, height - offset * 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
apply(context, controlValues, animState, renderData) {
|
||||||
|
const style = controlValues["emboss-style"] || "raised";
|
||||||
|
const depth = controlValues["emboss-depth"] || 2;
|
||||||
|
const lightColor = controlValues["emboss-light-color"] || "#ffffff";
|
||||||
|
const shadowColor = controlValues["emboss-shadow-color"] || "#000000";
|
||||||
|
|
||||||
|
const { width, height } = renderData;
|
||||||
|
|
||||||
|
context.save();
|
||||||
|
|
||||||
|
// Calculate alpha falloff for each layer (outer layers more opaque)
|
||||||
|
const getAlpha = (layer, totalLayers) => {
|
||||||
|
return 0.3 + (0.5 * (totalLayers - layer)) / totalLayers;
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (style) {
|
||||||
|
case "raised":
|
||||||
|
// Light on top/left, shadow on bottom/right
|
||||||
|
for (let i = 0; i < depth; i++) {
|
||||||
|
const alpha = getAlpha(i, depth);
|
||||||
|
this.drawBevel(
|
||||||
|
context,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
i,
|
||||||
|
lightColor,
|
||||||
|
shadowColor,
|
||||||
|
alpha,
|
||||||
|
alpha,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "sunken":
|
||||||
|
// Shadow on top/left, light on bottom/right (swap colors)
|
||||||
|
for (let i = 0; i < depth; i++) {
|
||||||
|
const alpha = getAlpha(i, depth);
|
||||||
|
this.drawBevel(
|
||||||
|
context,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
i,
|
||||||
|
shadowColor,
|
||||||
|
lightColor,
|
||||||
|
alpha,
|
||||||
|
alpha,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "ridge":
|
||||||
|
// Raised outer, sunken inner
|
||||||
|
const ridgeOuter = Math.ceil(depth / 2);
|
||||||
|
const ridgeInner = Math.floor(depth / 2);
|
||||||
|
|
||||||
|
// Outer raised bevel
|
||||||
|
for (let i = 0; i < ridgeOuter; i++) {
|
||||||
|
const alpha = getAlpha(i, ridgeOuter);
|
||||||
|
this.drawBevel(
|
||||||
|
context,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
i,
|
||||||
|
lightColor,
|
||||||
|
shadowColor,
|
||||||
|
alpha,
|
||||||
|
alpha,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inner sunken bevel
|
||||||
|
for (let i = 0; i < ridgeInner; i++) {
|
||||||
|
const alpha = getAlpha(i, ridgeInner);
|
||||||
|
this.drawBevel(
|
||||||
|
context,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
ridgeOuter + i,
|
||||||
|
shadowColor,
|
||||||
|
lightColor,
|
||||||
|
alpha,
|
||||||
|
alpha,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "groove":
|
||||||
|
// Sunken outer, raised inner (opposite of ridge)
|
||||||
|
const grooveOuter = Math.ceil(depth / 2);
|
||||||
|
const grooveInner = Math.floor(depth / 2);
|
||||||
|
|
||||||
|
// Outer sunken bevel
|
||||||
|
for (let i = 0; i < grooveOuter; i++) {
|
||||||
|
const alpha = getAlpha(i, grooveOuter);
|
||||||
|
this.drawBevel(
|
||||||
|
context,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
i,
|
||||||
|
shadowColor,
|
||||||
|
lightColor,
|
||||||
|
alpha,
|
||||||
|
alpha,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inner raised bevel
|
||||||
|
for (let i = 0; i < grooveInner; i++) {
|
||||||
|
const alpha = getAlpha(i, grooveInner);
|
||||||
|
this.drawBevel(
|
||||||
|
context,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
grooveOuter + i,
|
||||||
|
lightColor,
|
||||||
|
shadowColor,
|
||||||
|
alpha,
|
||||||
|
alpha,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
context.restore();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function register(generator) {
|
||||||
|
generator.registerEffect(new EmbossEffect());
|
||||||
|
}
|
||||||
187
static/js/button-generator/effects/text-bounce.js
Normal file
187
static/js/button-generator/effects/text-bounce.js
Normal file
|
|
@ -0,0 +1,187 @@
|
||||||
|
import { ButtonEffect } from "../effect-base.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bouncing text animation effect
|
||||||
|
* Characters bounce individually with staggered timing
|
||||||
|
*/
|
||||||
|
export class BounceTextEffect extends ButtonEffect {
|
||||||
|
constructor(textLineNumber = 1) {
|
||||||
|
const suffix = textLineNumber === 1 ? "" : "2";
|
||||||
|
super({
|
||||||
|
id: `text-bounce${suffix}`,
|
||||||
|
name: `Bouncing Text ${textLineNumber}`,
|
||||||
|
type: textLineNumber === 1 ? "text" : "text2",
|
||||||
|
category: textLineNumber === 1 ? "Text Line 1" : "Text Line 2",
|
||||||
|
renderOrder: 9, // Between spin (8) and wave (10)
|
||||||
|
textLineNumber: textLineNumber,
|
||||||
|
});
|
||||||
|
this.textLineNumber = textLineNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
defineControls() {
|
||||||
|
const textLineNumber =
|
||||||
|
this.textLineNumber || this.config?.textLineNumber || 1;
|
||||||
|
const suffix = textLineNumber === 1 ? "" : "2";
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: `animate-text-bounce${suffix}`,
|
||||||
|
type: "checkbox",
|
||||||
|
label: "Bounce Animation",
|
||||||
|
defaultValue: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: `bounce-height${suffix}`,
|
||||||
|
type: "range",
|
||||||
|
label: "Bounce Height",
|
||||||
|
defaultValue: 5,
|
||||||
|
min: 2,
|
||||||
|
max: 15,
|
||||||
|
step: 1,
|
||||||
|
showWhen: `animate-text-bounce${suffix}`,
|
||||||
|
description: "How high characters bounce",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: `bounce-speed${suffix}`,
|
||||||
|
type: "range",
|
||||||
|
label: "Bounce Speed",
|
||||||
|
defaultValue: 1.5,
|
||||||
|
min: 0.5,
|
||||||
|
max: 3,
|
||||||
|
step: 0.1,
|
||||||
|
showWhen: `animate-text-bounce${suffix}`,
|
||||||
|
description: "Speed of bounce animation",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: `bounce-stagger${suffix}`,
|
||||||
|
type: "range",
|
||||||
|
label: "Stagger",
|
||||||
|
defaultValue: 0.15,
|
||||||
|
min: 0,
|
||||||
|
max: 0.5,
|
||||||
|
step: 0.05,
|
||||||
|
showWhen: `animate-text-bounce${suffix}`,
|
||||||
|
description: "Delay between character bounces",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
isEnabled(controlValues) {
|
||||||
|
const suffix = this.textLineNumber === 1 ? "" : "2";
|
||||||
|
return controlValues[`animate-text-bounce${suffix}`] === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
apply(context, controlValues, animState, renderData) {
|
||||||
|
const suffix = this.textLineNumber === 1 ? "" : "2";
|
||||||
|
|
||||||
|
// Check if ticker is active - if so, ticker handles rendering
|
||||||
|
const tickerActive = controlValues[`animate-text-ticker${suffix}`];
|
||||||
|
if (tickerActive) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check flash visibility
|
||||||
|
const flashActive = controlValues[`animate-text-flash${suffix}`];
|
||||||
|
if (flashActive && renderData[`textFlashVisible${suffix}`] === false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = controlValues[`button-text${suffix}`] || "";
|
||||||
|
if (!text || text.trim() === "") return;
|
||||||
|
if (!animState) return;
|
||||||
|
|
||||||
|
const bounceHeight = controlValues[`bounce-height${suffix}`] || 5;
|
||||||
|
const speed = controlValues[`bounce-speed${suffix}`] || 1.5;
|
||||||
|
const stagger = controlValues[`bounce-stagger${suffix}`] || 0.15;
|
||||||
|
const fontSize = controlValues[`font-size${suffix}`] || 14;
|
||||||
|
const fontFamily = controlValues[`font-family${suffix}`] || "Arial";
|
||||||
|
const fontWeight = controlValues[`text${suffix}-bold`] ? "bold" : "normal";
|
||||||
|
const fontStyle = controlValues[`text${suffix}-italic`] ? "italic" : "normal";
|
||||||
|
|
||||||
|
context.font = `${fontStyle} ${fontWeight} ${fontSize}px "${fontFamily}"`;
|
||||||
|
context.textAlign = "center";
|
||||||
|
context.textBaseline = "middle";
|
||||||
|
|
||||||
|
// Get text color
|
||||||
|
let fillStyle;
|
||||||
|
const colorType = controlValues[`text${suffix}-color-type`] || "solid";
|
||||||
|
if (colorType === "gradient") {
|
||||||
|
const color1 = controlValues[`text${suffix}-gradient-color1`] || "#ffffff";
|
||||||
|
const color2 = controlValues[`text${suffix}-gradient-color2`] || "#00ffff";
|
||||||
|
const angle = (controlValues[`text${suffix}-gradient-angle`] || 90) * (Math.PI / 180);
|
||||||
|
const centerX = renderData.centerX;
|
||||||
|
const centerY = renderData.centerY;
|
||||||
|
const x1 = centerX + Math.cos(angle) * 20;
|
||||||
|
const y1 = centerY + Math.sin(angle) * 20;
|
||||||
|
const x2 = centerX - Math.cos(angle) * 20;
|
||||||
|
const y2 = centerY - Math.sin(angle) * 20;
|
||||||
|
const gradient = context.createLinearGradient(x1, y1, x2, y2);
|
||||||
|
gradient.addColorStop(0, color1);
|
||||||
|
gradient.addColorStop(1, color2);
|
||||||
|
fillStyle = gradient;
|
||||||
|
} else {
|
||||||
|
fillStyle = controlValues[`text${suffix}-color`] || "#ffffff";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for rainbow text effect
|
||||||
|
const rainbowActive = controlValues[`animate-text-rainbow${suffix}`];
|
||||||
|
|
||||||
|
// Calculate base position
|
||||||
|
const x = controlValues[`text${suffix}-x`] || 50;
|
||||||
|
const y = controlValues[`text${suffix}-y`] || 50;
|
||||||
|
const baseX = (x / 100) * renderData.width;
|
||||||
|
const baseY = (y / 100) * renderData.height;
|
||||||
|
|
||||||
|
// Split text into grapheme clusters (handles emojis properly)
|
||||||
|
let chars;
|
||||||
|
if (typeof Intl !== "undefined" && Intl.Segmenter) {
|
||||||
|
const segmenter = new Intl.Segmenter("en", { granularity: "grapheme" });
|
||||||
|
chars = Array.from(segmenter.segment(text), (s) => s.segment);
|
||||||
|
} else {
|
||||||
|
chars = [...text];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Measure total text width for centering
|
||||||
|
const totalWidth = context.measureText(text).width;
|
||||||
|
let currentX = baseX - totalWidth / 2;
|
||||||
|
|
||||||
|
// Draw each character with bounce
|
||||||
|
for (let i = 0; i < chars.length; i++) {
|
||||||
|
const char = chars[i];
|
||||||
|
const charWidth = context.measureText(char).width;
|
||||||
|
const charCenterX = currentX + charWidth / 2;
|
||||||
|
|
||||||
|
// Calculate bounce offset for this character
|
||||||
|
// Use abs(sin) for bounce motion (always goes up, sharp at bottom)
|
||||||
|
const phase = animState.getPhase(speed) + i * stagger * Math.PI * 2;
|
||||||
|
const bounceOffset = -Math.abs(Math.sin(phase)) * bounceHeight;
|
||||||
|
|
||||||
|
context.save();
|
||||||
|
|
||||||
|
// Apply outline if enabled
|
||||||
|
if (controlValues[`text${suffix}-outline`]) {
|
||||||
|
context.strokeStyle = controlValues[`text${suffix}-outline-color`] || "#000000";
|
||||||
|
context.lineWidth = 2;
|
||||||
|
context.strokeText(char, charCenterX, baseY + bounceOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle rainbow color per character
|
||||||
|
if (rainbowActive) {
|
||||||
|
const rainbowSpeed = controlValues[`text-rainbow-speed${suffix}`] || 1;
|
||||||
|
const hue = ((animState.progress * rainbowSpeed * 360) + (i * 30)) % 360;
|
||||||
|
context.fillStyle = `hsl(${hue}, 80%, 60%)`;
|
||||||
|
} else {
|
||||||
|
context.fillStyle = fillStyle;
|
||||||
|
}
|
||||||
|
|
||||||
|
context.fillText(char, charCenterX, baseY + bounceOffset);
|
||||||
|
context.restore();
|
||||||
|
|
||||||
|
currentX += charWidth;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function register(generator) {
|
||||||
|
generator.registerEffect(new BounceTextEffect(1));
|
||||||
|
generator.registerEffect(new BounceTextEffect(2));
|
||||||
|
}
|
||||||
202
static/js/button-generator/effects/text-glow.js
Normal file
202
static/js/button-generator/effects/text-glow.js
Normal file
|
|
@ -0,0 +1,202 @@
|
||||||
|
import { ButtonEffect } from "../effect-base.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Neon glow text effect
|
||||||
|
* Pulsing outer glow - cyberpunk/cool site aesthetic
|
||||||
|
*/
|
||||||
|
export class TextGlowEffect extends ButtonEffect {
|
||||||
|
constructor(textLineNumber = 1) {
|
||||||
|
const suffix = textLineNumber === 1 ? "" : "2";
|
||||||
|
super({
|
||||||
|
id: `text-glow${suffix}`,
|
||||||
|
name: `Glow Text ${textLineNumber}`,
|
||||||
|
type: textLineNumber === 1 ? "text" : "text2",
|
||||||
|
category: textLineNumber === 1 ? "Text Line 1" : "Text Line 2",
|
||||||
|
renderOrder: 18, // Before standard text at 20
|
||||||
|
textLineNumber: textLineNumber,
|
||||||
|
});
|
||||||
|
this.textLineNumber = textLineNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
defineControls() {
|
||||||
|
const textLineNumber =
|
||||||
|
this.textLineNumber || this.config?.textLineNumber || 1;
|
||||||
|
const suffix = textLineNumber === 1 ? "" : "2";
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: `animate-text-glow${suffix}`,
|
||||||
|
type: "checkbox",
|
||||||
|
label: "Neon Glow",
|
||||||
|
defaultValue: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: `glow-color${suffix}`,
|
||||||
|
type: "color",
|
||||||
|
label: "Glow Color",
|
||||||
|
defaultValue: "#00ffff",
|
||||||
|
showWhen: `animate-text-glow${suffix}`,
|
||||||
|
description: "Color of the glow effect",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: `glow-blur${suffix}`,
|
||||||
|
type: "range",
|
||||||
|
label: "Glow Size",
|
||||||
|
defaultValue: 8,
|
||||||
|
min: 2,
|
||||||
|
max: 20,
|
||||||
|
step: 1,
|
||||||
|
showWhen: `animate-text-glow${suffix}`,
|
||||||
|
description: "Size of the glow blur",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: `glow-intensity${suffix}`,
|
||||||
|
type: "range",
|
||||||
|
label: "Intensity",
|
||||||
|
defaultValue: 1,
|
||||||
|
min: 0.5,
|
||||||
|
max: 2,
|
||||||
|
step: 0.1,
|
||||||
|
showWhen: `animate-text-glow${suffix}`,
|
||||||
|
description: "Glow brightness",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: `glow-pulse${suffix}`,
|
||||||
|
type: "checkbox",
|
||||||
|
label: "Pulse Animation",
|
||||||
|
defaultValue: true,
|
||||||
|
showWhen: `animate-text-glow${suffix}`,
|
||||||
|
description: "Animate the glow",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: `glow-pulse-speed${suffix}`,
|
||||||
|
type: "range",
|
||||||
|
label: "Pulse Speed",
|
||||||
|
defaultValue: 1,
|
||||||
|
min: 0.5,
|
||||||
|
max: 3,
|
||||||
|
step: 0.1,
|
||||||
|
showWhen: `animate-text-glow${suffix}`,
|
||||||
|
description: "Speed of pulse animation",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
isEnabled(controlValues) {
|
||||||
|
const suffix = this.textLineNumber === 1 ? "" : "2";
|
||||||
|
return controlValues[`animate-text-glow${suffix}`] === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
apply(context, controlValues, animState, renderData) {
|
||||||
|
const suffix = this.textLineNumber === 1 ? "" : "2";
|
||||||
|
|
||||||
|
// Check if ticker is active - if so, ticker handles rendering
|
||||||
|
const tickerActive = controlValues[`animate-text-ticker${suffix}`];
|
||||||
|
if (tickerActive) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check flash visibility
|
||||||
|
const flashActive = controlValues[`animate-text-flash${suffix}`];
|
||||||
|
if (flashActive && renderData[`textFlashVisible${suffix}`] === false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = controlValues[`button-text${suffix}`] || "";
|
||||||
|
if (!text || text.trim() === "") return;
|
||||||
|
|
||||||
|
const glowColor = controlValues[`glow-color${suffix}`] || "#00ffff";
|
||||||
|
const baseBlur = controlValues[`glow-blur${suffix}`] || 8;
|
||||||
|
const intensity = controlValues[`glow-intensity${suffix}`] || 1;
|
||||||
|
const pulse = controlValues[`glow-pulse${suffix}`] !== false;
|
||||||
|
const pulseSpeed = controlValues[`glow-pulse-speed${suffix}`] || 1;
|
||||||
|
|
||||||
|
// Calculate pulse multiplier
|
||||||
|
let pulseMultiplier = 1;
|
||||||
|
if (pulse && animState) {
|
||||||
|
const phase = animState.getPhase(pulseSpeed);
|
||||||
|
pulseMultiplier = 0.6 + Math.sin(phase) * 0.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
const blur = baseBlur * pulseMultiplier;
|
||||||
|
|
||||||
|
// Get font settings
|
||||||
|
const fontSize = controlValues[`font-size${suffix}`] || 14;
|
||||||
|
const fontFamily = controlValues[`font-family${suffix}`] || "Arial";
|
||||||
|
const fontWeight = controlValues[`text${suffix}-bold`] ? "bold" : "normal";
|
||||||
|
const fontStyle = controlValues[`text${suffix}-italic`] ? "italic" : "normal";
|
||||||
|
|
||||||
|
context.font = `${fontStyle} ${fontWeight} ${fontSize}px "${fontFamily}"`;
|
||||||
|
context.textAlign = "center";
|
||||||
|
context.textBaseline = "middle";
|
||||||
|
|
||||||
|
// Get text color
|
||||||
|
let textColor;
|
||||||
|
const colorType = controlValues[`text${suffix}-color-type`] || "solid";
|
||||||
|
if (colorType === "gradient") {
|
||||||
|
textColor = controlValues[`text${suffix}-gradient-color1`] || "#ffffff";
|
||||||
|
} else {
|
||||||
|
textColor = controlValues[`text${suffix}-color`] || "#ffffff";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate position
|
||||||
|
const x = controlValues[`text${suffix}-x`] || 50;
|
||||||
|
const y = controlValues[`text${suffix}-y`] || 50;
|
||||||
|
const posX = (x / 100) * renderData.width;
|
||||||
|
const posY = (y / 100) * renderData.height;
|
||||||
|
|
||||||
|
context.save();
|
||||||
|
|
||||||
|
// Draw glow layers (multiple passes for stronger glow)
|
||||||
|
const passes = Math.ceil(intensity * 3);
|
||||||
|
for (let i = 0; i < passes; i++) {
|
||||||
|
context.shadowColor = glowColor;
|
||||||
|
context.shadowBlur = blur * (1 + i * 0.3);
|
||||||
|
context.shadowOffsetX = 0;
|
||||||
|
context.shadowOffsetY = 0;
|
||||||
|
context.fillStyle = glowColor;
|
||||||
|
context.globalAlpha = 0.3 * intensity * pulseMultiplier;
|
||||||
|
context.fillText(text, posX, posY);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw outline if enabled
|
||||||
|
if (controlValues[`text${suffix}-outline`]) {
|
||||||
|
context.globalAlpha = 1;
|
||||||
|
context.shadowBlur = 0;
|
||||||
|
context.strokeStyle = controlValues[`text${suffix}-outline-color`] || "#000000";
|
||||||
|
context.lineWidth = 2;
|
||||||
|
context.strokeText(text, posX, posY);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw main text with subtle glow
|
||||||
|
context.globalAlpha = 1;
|
||||||
|
context.shadowColor = glowColor;
|
||||||
|
context.shadowBlur = blur * 0.5;
|
||||||
|
|
||||||
|
// Handle gradient fill
|
||||||
|
if (colorType === "gradient") {
|
||||||
|
const color1 = controlValues[`text${suffix}-gradient-color1`] || "#ffffff";
|
||||||
|
const color2 = controlValues[`text${suffix}-gradient-color2`] || "#00ffff";
|
||||||
|
const angle = (controlValues[`text${suffix}-gradient-angle`] || 90) * (Math.PI / 180);
|
||||||
|
const textWidth = context.measureText(text).width;
|
||||||
|
const x1 = posX + Math.cos(angle) * textWidth * 0.5;
|
||||||
|
const y1 = posY + Math.sin(angle) * fontSize * 0.5;
|
||||||
|
const x2 = posX - Math.cos(angle) * textWidth * 0.5;
|
||||||
|
const y2 = posY - Math.sin(angle) * fontSize * 0.5;
|
||||||
|
const gradient = context.createLinearGradient(x1, y1, x2, y2);
|
||||||
|
gradient.addColorStop(0, color1);
|
||||||
|
gradient.addColorStop(1, color2);
|
||||||
|
context.fillStyle = gradient;
|
||||||
|
} else {
|
||||||
|
context.fillStyle = textColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
context.fillText(text, posX, posY);
|
||||||
|
|
||||||
|
context.restore();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function register(generator) {
|
||||||
|
generator.registerEffect(new TextGlowEffect(1));
|
||||||
|
generator.registerEffect(new TextGlowEffect(2));
|
||||||
|
}
|
||||||
|
|
@ -125,6 +125,10 @@ export class StandardTextEffect extends ButtonEffect {
|
||||||
{ value: "VT323", label: "VT323" },
|
{ value: "VT323", label: "VT323" },
|
||||||
{ value: "Press Start 2P", label: "Press Start 2P" },
|
{ value: "Press Start 2P", label: "Press Start 2P" },
|
||||||
{ value: "DSEG7-Classic", label: "DSEG7" },
|
{ value: "DSEG7-Classic", label: "DSEG7" },
|
||||||
|
{ value: "Pixelify Sans", label: "Pixelify Sans" },
|
||||||
|
{ value: "Bungee Spice", label: "Bungee Spice" },
|
||||||
|
{ value: "Creepster", label: "Creepster" },
|
||||||
|
{ value: "Barrio", label: "Barrio" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -151,8 +155,19 @@ export class StandardTextEffect extends ButtonEffect {
|
||||||
const rainbowActive = controlValues[`animate-text-rainbow${suffix}`];
|
const rainbowActive = controlValues[`animate-text-rainbow${suffix}`];
|
||||||
const spinActive = controlValues[`animate-text-spin${suffix}`];
|
const spinActive = controlValues[`animate-text-spin${suffix}`];
|
||||||
const tickerActive = controlValues[`animate-text-ticker${suffix}`];
|
const tickerActive = controlValues[`animate-text-ticker${suffix}`];
|
||||||
|
const bounceActive = controlValues[`animate-text-bounce${suffix}`];
|
||||||
|
const glowActive = controlValues[`animate-text-glow${suffix}`];
|
||||||
|
|
||||||
return text && text.trim() !== "" && !waveActive && !rainbowActive && !spinActive && !tickerActive;
|
return (
|
||||||
|
text &&
|
||||||
|
text.trim() !== "" &&
|
||||||
|
!waveActive &&
|
||||||
|
!rainbowActive &&
|
||||||
|
!spinActive &&
|
||||||
|
!tickerActive &&
|
||||||
|
!bounceActive &&
|
||||||
|
!glowActive
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
apply(context, controlValues, animState, renderData) {
|
apply(context, controlValues, animState, renderData) {
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,11 @@ import * as noise from "./effects/noise.js";
|
||||||
import * as rotate from "./effects/rotate.js";
|
import * as rotate from "./effects/rotate.js";
|
||||||
import * as hologram from "./effects/hologram.js";
|
import * as hologram from "./effects/hologram.js";
|
||||||
import * as spotlight from "./effects/spotlight.js";
|
import * as spotlight from "./effects/spotlight.js";
|
||||||
|
import * as sparkle from "./effects/background-sparkle.js";
|
||||||
|
import * as textGlow from "./effects/text-glow.js";
|
||||||
|
import * as plasma from "./effects/background-plasma.js";
|
||||||
|
import * as emboss from "./effects/emboss.js";
|
||||||
|
import * as bounceText from "./effects/text-bounce.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize the button generator application
|
* Initialize the button generator application
|
||||||
|
|
@ -71,7 +76,6 @@ async function setupApp() {
|
||||||
fps: 20,
|
fps: 20,
|
||||||
duration: 2,
|
duration: 2,
|
||||||
fonts: [
|
fonts: [
|
||||||
"Arial",
|
|
||||||
"Lato",
|
"Lato",
|
||||||
"Roboto",
|
"Roboto",
|
||||||
"Open Sans",
|
"Open Sans",
|
||||||
|
|
@ -82,6 +86,10 @@ async function setupApp() {
|
||||||
"VT323",
|
"VT323",
|
||||||
"Press Start 2P",
|
"Press Start 2P",
|
||||||
"DSEG7-Classic",
|
"DSEG7-Classic",
|
||||||
|
"Pixelify Sans",
|
||||||
|
"Bungee Spice",
|
||||||
|
"Creepster",
|
||||||
|
"Barrio",
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -115,6 +123,11 @@ async function setupApp() {
|
||||||
rotate.register(generator);
|
rotate.register(generator);
|
||||||
hologram.register(generator);
|
hologram.register(generator);
|
||||||
spotlight.register(generator);
|
spotlight.register(generator);
|
||||||
|
sparkle.register(generator);
|
||||||
|
textGlow.register(generator);
|
||||||
|
plasma.register(generator);
|
||||||
|
emboss.register(generator);
|
||||||
|
bounceText.register(generator);
|
||||||
|
|
||||||
console.log(`Registered ${generator.getAllEffects().length} effects`);
|
console.log(`Registered ${generator.getAllEffects().length} effects`);
|
||||||
|
|
||||||
|
|
@ -159,21 +172,21 @@ async function setupApp() {
|
||||||
* Add GIF export settings controls
|
* Add GIF export settings controls
|
||||||
*/
|
*/
|
||||||
function addGifExportSettings(container, generator) {
|
function addGifExportSettings(container, generator) {
|
||||||
const groupDiv = document.createElement('div');
|
const groupDiv = document.createElement("div");
|
||||||
groupDiv.className = 'control-group collapsed';
|
groupDiv.className = "control-group collapsed";
|
||||||
|
|
||||||
// Header
|
// Header
|
||||||
const header = document.createElement('h3');
|
const header = document.createElement("h3");
|
||||||
header.className = 'control-group-header';
|
header.className = "control-group-header";
|
||||||
header.textContent = 'Advanced Settings';
|
header.textContent = "Advanced Settings";
|
||||||
groupDiv.appendChild(header);
|
groupDiv.appendChild(header);
|
||||||
|
|
||||||
const controlsDiv = document.createElement('div');
|
const controlsDiv = document.createElement("div");
|
||||||
controlsDiv.className = 'control-group-controls';
|
controlsDiv.className = "control-group-controls";
|
||||||
|
|
||||||
// Color count control
|
// Color count control
|
||||||
const colorsWrapper = document.createElement('div');
|
const colorsWrapper = document.createElement("div");
|
||||||
colorsWrapper.className = 'control-wrapper';
|
colorsWrapper.className = "control-wrapper";
|
||||||
colorsWrapper.innerHTML = `
|
colorsWrapper.innerHTML = `
|
||||||
<div class="info-text">Note: This only affects frame-by-frame settings, i.e. 8 colours would be 8 colours per frame. I am working on a solution for this.</div>
|
<div class="info-text">Note: This only affects frame-by-frame settings, i.e. 8 colours would be 8 colours per frame. I am working on a solution for this.</div>
|
||||||
<label for="gif-colors">
|
<label for="gif-colors">
|
||||||
|
|
@ -188,8 +201,8 @@ function addGifExportSettings(container, generator) {
|
||||||
controlsDiv.appendChild(colorsWrapper);
|
controlsDiv.appendChild(colorsWrapper);
|
||||||
|
|
||||||
// Quality control
|
// Quality control
|
||||||
const qualityWrapper = document.createElement('div');
|
const qualityWrapper = document.createElement("div");
|
||||||
qualityWrapper.className = 'control-wrapper';
|
qualityWrapper.className = "control-wrapper";
|
||||||
qualityWrapper.innerHTML = `
|
qualityWrapper.innerHTML = `
|
||||||
<label for="gif-quality">
|
<label for="gif-quality">
|
||||||
Quantization Quality
|
Quantization Quality
|
||||||
|
|
@ -203,8 +216,8 @@ function addGifExportSettings(container, generator) {
|
||||||
controlsDiv.appendChild(qualityWrapper);
|
controlsDiv.appendChild(qualityWrapper);
|
||||||
|
|
||||||
// Dither control
|
// Dither control
|
||||||
const ditherWrapper = document.createElement('div');
|
const ditherWrapper = document.createElement("div");
|
||||||
ditherWrapper.className = 'control-wrapper';
|
ditherWrapper.className = "control-wrapper";
|
||||||
ditherWrapper.innerHTML = `
|
ditherWrapper.innerHTML = `
|
||||||
<label for="gif-dither">
|
<label for="gif-dither">
|
||||||
Dithering Pattern
|
Dithering Pattern
|
||||||
|
|
@ -224,38 +237,38 @@ function addGifExportSettings(container, generator) {
|
||||||
container.appendChild(groupDiv);
|
container.appendChild(groupDiv);
|
||||||
|
|
||||||
// Setup event listeners for value display
|
// Setup event listeners for value display
|
||||||
const colorsInput = document.getElementById('gif-colors');
|
const colorsInput = document.getElementById("gif-colors");
|
||||||
const colorsValue = document.getElementById('gif-colors-value');
|
const colorsValue = document.getElementById("gif-colors-value");
|
||||||
colorsInput.addEventListener('input', () => {
|
colorsInput.addEventListener("input", () => {
|
||||||
colorsValue.textContent = colorsInput.value;
|
colorsValue.textContent = colorsInput.value;
|
||||||
});
|
});
|
||||||
|
|
||||||
const qualityInput = document.getElementById('gif-quality');
|
const qualityInput = document.getElementById("gif-quality");
|
||||||
const qualityValue = document.getElementById('gif-quality-value');
|
const qualityValue = document.getElementById("gif-quality-value");
|
||||||
qualityInput.addEventListener('input', () => {
|
qualityInput.addEventListener("input", () => {
|
||||||
qualityValue.textContent = qualityInput.value;
|
qualityValue.textContent = qualityInput.value;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update generator config when changed
|
// Update generator config when changed
|
||||||
colorsInput.addEventListener('change', () => {
|
colorsInput.addEventListener("change", () => {
|
||||||
generator.gifConfig.colorCount = parseInt(colorsInput.value);
|
generator.gifConfig.colorCount = parseInt(colorsInput.value);
|
||||||
generator.updatePreview(); // Update preview to show color quantization
|
generator.updatePreview(); // Update preview to show color quantization
|
||||||
});
|
});
|
||||||
|
|
||||||
// Also update preview on slider input for real-time feedback
|
// Also update preview on slider input for real-time feedback
|
||||||
colorsInput.addEventListener('input', () => {
|
colorsInput.addEventListener("input", () => {
|
||||||
generator.gifConfig.colorCount = parseInt(colorsInput.value);
|
generator.gifConfig.colorCount = parseInt(colorsInput.value);
|
||||||
generator.updatePreview(); // Real-time preview update
|
generator.updatePreview(); // Real-time preview update
|
||||||
});
|
});
|
||||||
|
|
||||||
qualityInput.addEventListener('change', () => {
|
qualityInput.addEventListener("change", () => {
|
||||||
generator.gifConfig.quality = parseInt(qualityInput.value);
|
generator.gifConfig.quality = parseInt(qualityInput.value);
|
||||||
});
|
});
|
||||||
|
|
||||||
const ditherSelect = document.getElementById('gif-dither');
|
const ditherSelect = document.getElementById("gif-dither");
|
||||||
ditherSelect.addEventListener('change', () => {
|
ditherSelect.addEventListener("change", () => {
|
||||||
const value = ditherSelect.value;
|
const value = ditherSelect.value;
|
||||||
generator.gifConfig.dither = value === 'false' ? false : value;
|
generator.gifConfig.dither = value === "false" ? false : value;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue