(function () { // Wait for DOM to be ready if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", init); } else { init(); } function init() { setupCollapsible(); setupButtonGenerator(); } // Collapsible sections 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"); console.log("Toggled collapsed on", header.textContent.trim()); } }); }); } function setupButtonGenerator() { const canvas = document.getElementById("button-canvas"); const ctx = canvas.getContext("2d"); // Preload all web fonts for canvas rendering const fonts = [ "Lato", "Roboto", "Open Sans", "Montserrat", "Oswald", "Bebas Neue", "Roboto Mono", "VT323", "Press Start 2P", "DSEG7-Classic", ]; // Load fonts using CSS Font Loading API const fontPromises = fonts.flatMap((font) => [ document.fonts.load(`400 12px "${font}"`), document.fonts.load(`700 12px "${font}"`), document.fonts.load(`italic 400 12px "${font}"`), ]); Promise.all(fontPromises).then(() => { console.log("All fonts loaded for canvas"); drawButton(); }); // Animation configuration const ANIMATION_CONFIG = { fps: 20, duration: 2, // seconds get totalFrames() { return this.fps * this.duration; }, // 40 frames }; // Animation state class - passed to drawToContext for frame-based rendering class AnimationState { constructor(frameNumber = 0, totalFrames = 40) { this.frame = frameNumber; this.totalFrames = totalFrames; this.progress = frameNumber / totalFrames; // 0 to 1 this.time = (frameNumber / ANIMATION_CONFIG.fps) * 1000; // milliseconds } // Helper to get phase for periodic animations (0 to 2π) getPhase(speed = 1.0) { return this.progress * speed * Math.PI * 2; } } // Get all controls const controls = { text: document.getElementById("button-text"), textEnabled: document.getElementById("text-enabled"), fontSize: document.getElementById("font-size"), textX: document.getElementById("text-x"), textY: document.getElementById("text-y"), textColorType: document.getElementById("text-color-type"), textColor: document.getElementById("text-color"), textGradientColor1: document.getElementById("text-gradient-color1"), textGradientColor2: document.getElementById("text-gradient-color2"), textGradientAngle: document.getElementById("text-gradient-angle"), textOutline: document.getElementById("text-outline"), outlineColor: document.getElementById("outline-color"), fontFamily: document.getElementById("font-family"), fontBold: document.getElementById("font-bold"), fontItalic: document.getElementById("font-italic"), text2: document.getElementById("button-text2"), text2Enabled: document.getElementById("text2-enabled"), fontSize2: document.getElementById("font-size2"), text2X: document.getElementById("text2-x"), text2Y: document.getElementById("text2-y"), text2ColorType: document.getElementById("text2-color-type"), text2Color: document.getElementById("text2-color"), text2GradientColor1: document.getElementById("text2-gradient-color1"), text2GradientColor2: document.getElementById("text2-gradient-color2"), text2GradientAngle: document.getElementById("text2-gradient-angle"), text2Outline: document.getElementById("text2-outline"), outline2Color: document.getElementById("outline2-color"), fontFamily2: document.getElementById("font-family2"), fontBold2: document.getElementById("font-bold2"), fontItalic2: document.getElementById("font-italic2"), bgType: document.getElementById("bg-type"), bgColor: document.getElementById("bg-color"), gradientColor1: document.getElementById("gradient-color1"), gradientColor2: document.getElementById("gradient-color2"), gradientAngle: document.getElementById("gradient-angle"), textureType: document.getElementById("texture-type"), textureColor1: document.getElementById("texture-color1"), textureColor2: document.getElementById("texture-color2"), textureScale: document.getElementById("texture-scale"), borderWidth: document.getElementById("border-width"), borderColor: document.getElementById("border-color"), borderStyle: document.getElementById("border-style"), // Animation controls animateTextWave: document.getElementById("animate-text-wave"), waveAmplitude: document.getElementById("wave-amplitude"), waveSpeed: document.getElementById("wave-speed"), animateTextWave2: document.getElementById("animate-text-wave2"), waveAmplitude2: document.getElementById("wave-amplitude2"), waveSpeed2: document.getElementById("wave-speed2"), animateBgRainbow: document.getElementById("animate-bg-rainbow"), rainbowSpeed: document.getElementById("rainbow-speed"), animateBgRainbowGradient: document.getElementById( "animate-bg-rainbow-gradient", ), animateTextRainbow: document.getElementById("animate-text-rainbow"), textRainbowSpeed: document.getElementById("text-rainbow-speed"), animateTextRainbow2: document.getElementById("animate-text-rainbow2"), textRainbowSpeed2: document.getElementById("text-rainbow-speed2"), animateGlitch: document.getElementById("animate-glitch"), glitchIntensity: document.getElementById("glitch-intensity"), animatePulse: document.getElementById("animate-pulse"), pulseScale: document.getElementById("pulse-scale"), animateShimmer: document.getElementById("animate-shimmer"), animateScanline: document.getElementById("animate-scanline"), scanlineIntensity: document.getElementById("scanline-intensity"), scanlineSpeed: document.getElementById("scanline-speed"), animateRgbSplit: document.getElementById("animate-rgb-split"), rgbSplitIntensity: document.getElementById("rgb-split-intensity"), animateNoise: document.getElementById("animate-noise"), noiseIntensity: document.getElementById("noise-intensity"), animateRotate: document.getElementById("animate-rotate"), rotateAngle: document.getElementById("rotate-angle"), rotateSpeed: document.getElementById("rotate-speed"), }; // Update value displays const updateValueDisplay = (id, value) => { const display = document.getElementById(id + "-value"); if (display) display.textContent = value; }; // Show/hide controls based on background type controls.bgType.addEventListener("change", () => { const solidControls = document.getElementById("solid-controls"); const gradientControls = document.getElementById("gradient-controls"); const textureControls = document.getElementById("texture-controls"); solidControls.style.display = "none"; gradientControls.style.display = "none"; textureControls.style.display = "none"; if (controls.bgType.value === "solid") { solidControls.style.display = "block"; } else if (controls.bgType.value === "gradient") { gradientControls.style.display = "block"; } else if (controls.bgType.value === "texture") { textureControls.style.display = "block"; } drawButton(); }); // Show/hide text color controls controls.textColorType.addEventListener("change", () => { const textSolidColor = document.getElementById("text-solid-color"); const textGradientColor = document.getElementById("text-gradient-color"); if (controls.textColorType.value === "solid") { textSolidColor.style.display = "block"; textGradientColor.style.display = "none"; } else { textSolidColor.style.display = "none"; textGradientColor.style.display = "block"; } drawButton(); }); controls.text2ColorType.addEventListener("change", () => { const text2SolidColor = document.getElementById("text2-solid-color"); const text2GradientColor = document.getElementById( "text2-gradient-color", ); if (controls.text2ColorType.value === "solid") { text2SolidColor.style.display = "block"; text2GradientColor.style.display = "none"; } else { text2SolidColor.style.display = "none"; text2GradientColor.style.display = "block"; } drawButton(); }); // Show/hide outline color controls.textOutline.addEventListener("change", () => { controls.outlineColor.style.display = controls.textOutline.checked ? "block" : "none"; drawButton(); }); controls.text2Outline.addEventListener("change", () => { controls.outline2Color.style.display = controls.text2Outline.checked ? "block" : "none"; drawButton(); }); // Draw texture patterns function drawTexture(type, color1, color2, scale) { const tempCanvas = document.createElement("canvas"); tempCanvas.width = 88; tempCanvas.height = 31; const tempCtx = tempCanvas.getContext("2d"); tempCtx.fillStyle = color1; tempCtx.fillRect(0, 0, 88, 31); tempCtx.fillStyle = color2; const size = Math.max(2, Math.floor(scale / 10)); switch (type) { case "dots": for (let y = 0; y < 31; y += size * 2) { for (let x = 0; x < 88; x += size * 2) { tempCtx.beginPath(); tempCtx.arc(x, y, size / 2, 0, Math.PI * 2); tempCtx.fill(); } } break; case "grid": for (let x = 0; x < 88; x += size) { tempCtx.fillRect(x, 0, 1, 31); } for (let y = 0; y < 31; y += size) { tempCtx.fillRect(0, y, 88, 1); } break; case "diagonal": for (let i = -31; i < 88; i += size) { tempCtx.fillRect(i, 0, 2, 31); tempCtx.save(); tempCtx.translate(i + 1, 0); tempCtx.rotate(Math.PI / 4); tempCtx.fillRect(0, 0, 2, 100); tempCtx.restore(); } break; case "checkerboard": for (let y = 0; y < 31; y += size) { for (let x = 0; x < 88; x += size) { if ((x / size + y / size) % 2 === 0) { tempCtx.fillRect(x, y, size, size); } } } break; case "noise": for (let y = 0; y < 31; y++) { for (let x = 0; x < 88; x++) { if (Math.random() > 0.5) { tempCtx.fillRect(x, y, 1, 1); } } } break; case "stars": for (let i = 0; i < scale; i++) { const x = Math.floor(Math.random() * 88); const y = Math.floor(Math.random() * 31); tempCtx.fillRect(x, y, 1, 1); tempCtx.fillRect(x - 1, y, 1, 1); tempCtx.fillRect(x + 1, y, 1, 1); tempCtx.fillRect(x, y - 1, 1, 1); tempCtx.fillRect(x, y + 1, 1, 1); } break; } return tempCanvas; } // Helper function to draw text line with optional wave animation function drawTextLine(context, lineNumber, animState) { const prefix = lineNumber === 1 ? "" : "2"; const text = controls[`text${prefix}`].value; const enabled = controls[`text${prefix}Enabled`].checked; if (!text || !enabled) return; const fontSize = parseFloat(controls[`fontSize${prefix}`].value); const fontWeight = controls[`fontBold${prefix}`].checked ? "bold" : "normal"; const fontStyle = controls[`fontItalic${prefix}`].checked ? "italic" : "normal"; const fontFamily = controls[`fontFamily${prefix}`].value; context.font = `${fontStyle} ${fontWeight} ${fontSize}px "${fontFamily}"`; context.textAlign = "center"; context.textBaseline = "middle"; const baseX = (parseFloat(controls[`text${prefix}X`].value) / 100) * 88; const baseY = (parseFloat(controls[`text${prefix}Y`].value) / 100) * 31; // Check if wave animation is enabled const waveEnabled = animState && controls[`animateTextWave${prefix}`]?.checked; if (waveEnabled) { drawWaveText( context, text, baseX, baseY, fontSize, animState, lineNumber, ); } else { drawStandardText( context, text, baseX, baseY, fontSize, animState, lineNumber, ); } } // Draw standard (non-wave) text function drawStandardText( context, text, x, y, fontSize, animState, lineNumber, ) { const prefix = lineNumber === 1 ? "" : "2"; // Get colors (with potential rainbow animation) const colors = getTextColors( lineNumber, animState, context, text, x, y, fontSize, ); if (controls[`text${prefix}Outline`].checked) { context.strokeStyle = colors.strokeStyle; context.lineWidth = 2; context.strokeText(text, x, y); } context.fillStyle = colors.fillStyle; context.fillText(text, x, y); } // Draw wave-animated text (character by character) function drawWaveText( context, text, baseX, baseY, fontSize, animState, lineNumber, ) { const prefix = lineNumber === 1 ? "" : "2"; const amplitude = parseFloat(controls[`waveAmplitude${prefix}`].value); const speed = parseFloat(controls[`waveSpeed${prefix}`].value); // Measure total width for centering const totalWidth = context.measureText(text).width; let currentX = baseX - totalWidth / 2; 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; // 0 to 1 const waveY = Math.sin(phase + charOffset * Math.PI * 2) * amplitude; const charX = currentX + charWidth / 2; const charY = baseY + waveY; // Get colors for this character const colors = getTextColors( lineNumber, animState, context, char, charX, charY, fontSize, ); // Draw outline if enabled if (controls[`text${prefix}Outline`].checked) { 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) function getTextColors( lineNumber, animState, context, text, x, y, fontSize, ) { const prefix = lineNumber === 1 ? "" : "2"; const colorType = controls[`text${prefix}ColorType`].value; let fillStyle, strokeStyle; // Rainbow text animation overrides other colors if (animState && controls[`animateTextRainbow${prefix}`]?.checked) { const hue = (animState.progress * parseFloat(controls[`textRainbowSpeed${prefix}`].value) * 360) % 360; fillStyle = `hsl(${hue}, 80%, 60%)`; strokeStyle = `hsl(${hue}, 80%, 30%)`; } else if (colorType === "solid") { fillStyle = controls[`text${prefix}Color`].value; strokeStyle = controls[`outline${prefix}Color`].value; } else { // Gradient const angle = parseFloat(controls[`text${prefix}GradientAngle`].value) * (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 textGradient = context.createLinearGradient(x1, y1, x2, y2); textGradient.addColorStop( 0, controls[`text${prefix}GradientColor1`].value, ); textGradient.addColorStop( 1, controls[`text${prefix}GradientColor2`].value, ); fillStyle = textGradient; strokeStyle = controls[`outline${prefix}Color`].value; } return { fillStyle, strokeStyle }; } // Helper function to draw to a single context function drawToContext(context, animState = null) { context.clearRect(0, 0, 88, 31); // Apply rotate effect (must be before drawing) if (animState && controls.animateRotate?.checked) { const maxAngle = parseFloat(controls.rotateAngle.value); const speed = parseFloat(controls.rotateSpeed.value); const angle = Math.sin(animState.getPhase(speed)) * maxAngle * (Math.PI / 180); context.save(); context.translate(44, 15.5); context.rotate(angle); context.translate(-44, -15.5); } // Draw background if (controls.bgType.value === "solid") { // Rainbow flash for solid colors if (animState && controls.animateBgRainbow?.checked) { const hue = (animState.progress * parseFloat(controls.rainbowSpeed.value) * 360) % 360; context.fillStyle = `hsl(${hue}, 70%, 50%)`; } else { context.fillStyle = controls.bgColor.value; } context.fillRect(0, 0, 88, 31); } else if (controls.bgType.value === "gradient") { const angle = parseFloat(controls.gradientAngle.value) * (Math.PI / 180); const x1 = 44 + Math.cos(angle) * 44; const y1 = 15.5 + Math.sin(angle) * 15.5; const x2 = 44 - Math.cos(angle) * 44; const y2 = 15.5 - Math.sin(angle) * 15.5; const gradient = context.createLinearGradient(x1, y1, x2, y2); // Rainbow flash for gradients if (animState && controls.animateBgRainbow?.checked) { const hue = (animState.progress * parseFloat(controls.rainbowSpeed.value) * 360) % 360; gradient.addColorStop(0, `hsl(${hue}, 70%, 50%)`); gradient.addColorStop(1, `hsl(${(hue + 60) % 360}, 70%, 60%)`); } else { gradient.addColorStop(0, controls.gradientColor1.value); gradient.addColorStop(1, controls.gradientColor2.value); } context.fillStyle = gradient; context.fillRect(0, 0, 88, 31); } else if (controls.bgType.value === "texture") { const texture = drawTexture( controls.textureType.value, controls.textureColor1.value, controls.textureColor2.value, parseFloat(controls.textureScale.value), ); context.drawImage(texture, 0, 0); } // Rainbow gradient overlay (travels across background) if (animState && controls.animateBgRainbowGradient?.checked) { // Map progress to position (-100 to 100) const position = animState.progress * 200 - 100; // Create a horizontal gradient that sweeps across const rainbowGradient = context.createLinearGradient( position - 50, 0, position + 50, 0, ); // Create rainbow stops that also cycle through colors const hueOffset = animState.progress * 360; rainbowGradient.addColorStop( 0, `hsla(${(hueOffset + 0) % 360}, 80%, 50%, 0)`, ); rainbowGradient.addColorStop( 0.2, `hsla(${(hueOffset + 60) % 360}, 80%, 50%, 0.6)`, ); rainbowGradient.addColorStop( 0.4, `hsla(${(hueOffset + 120) % 360}, 80%, 50%, 0.8)`, ); rainbowGradient.addColorStop( 0.6, `hsla(${(hueOffset + 180) % 360}, 80%, 50%, 0.8)`, ); rainbowGradient.addColorStop( 0.8, `hsla(${(hueOffset + 240) % 360}, 80%, 50%, 0.6)`, ); rainbowGradient.addColorStop( 1, `hsla(${(hueOffset + 300) % 360}, 80%, 50%, 0)`, ); context.fillStyle = rainbowGradient; context.fillRect(0, 0, 88, 31); } // Draw border const borderWidth = parseFloat(controls.borderWidth.value); if (borderWidth > 0) { const style = controls.borderStyle.value; if (style === "solid") { context.strokeStyle = controls.borderColor.value; context.lineWidth = borderWidth; context.strokeRect( borderWidth / 2, borderWidth / 2, 88 - borderWidth, 31 - borderWidth, ); } else if (style === "inset" || style === "outset") { const light = style === "outset"; context.strokeStyle = light ? "#ffffff" : "#000000"; context.lineWidth = borderWidth; context.beginPath(); context.moveTo(0, 31); context.lineTo(0, 0); context.lineTo(88, 0); context.stroke(); context.strokeStyle = light ? "#000000" : "#ffffff"; context.beginPath(); context.moveTo(88, 0); context.lineTo(88, 31); context.lineTo(0, 31); context.stroke(); } else if (style === "ridge") { context.strokeStyle = "#ffffff"; context.lineWidth = borderWidth / 2; context.strokeRect( borderWidth / 4, borderWidth / 4, 88 - borderWidth / 2, 31 - borderWidth / 2, ); context.strokeStyle = "#000000"; context.strokeRect( (borderWidth * 3) / 4, (borderWidth * 3) / 4, 88 - borderWidth * 1.5, 31 - borderWidth * 1.5, ); } } // Apply pulse effect (scale before drawing text) if (animState && controls.animatePulse?.checked) { const maxScale = parseFloat(controls.pulseScale.value); const minScale = 1.0; const scale = minScale + (maxScale - minScale) * (Math.sin(animState.getPhase(1.0)) * 0.5 + 0.5); context.save(); context.translate(44, 15.5); context.scale(scale, scale); context.translate(-44, -15.5); } // Draw text line 1 drawTextLine(context, 1, animState); // Draw text line 2 drawTextLine(context, 2, animState); // Restore context if pulse was applied if (animState && controls.animatePulse?.checked) { context.restore(); } // Apply shimmer effect if (animState && controls.animateShimmer?.checked) { const shimmerX = animState.progress * 120 - 20; // Sweep from -20 to 100 const shimmerGradient = context.createLinearGradient( shimmerX - 15, 0, shimmerX + 15, 31, ); shimmerGradient.addColorStop(0, "rgba(255, 255, 255, 0)"); shimmerGradient.addColorStop(0.5, "rgba(255, 255, 255, 0.3)"); shimmerGradient.addColorStop(1, "rgba(255, 255, 255, 0)"); context.fillStyle = shimmerGradient; context.fillRect(0, 0, 88, 31); } // Apply glitch effect if (animState && controls.animateGlitch?.checked) { applyGlitchEffect(context, animState); } // Apply scanline effect if (animState && controls.animateScanline?.checked) { applyScanlineEffect(context, animState); } // Apply RGB split effect if (animState && controls.animateRgbSplit?.checked) { applyRgbSplitEffect(context, animState); } // Apply noise effect if (animState && controls.animateNoise?.checked) { applyNoiseEffect(context, animState); } // Restore context if rotate was applied if (animState && controls.animateRotate?.checked) { context.restore(); } } // Apply glitch effect (scanline displacement) function applyGlitchEffect(context, animState) { const intensity = parseFloat(controls.glitchIntensity.value); const imageData = context.getImageData(0, 0, 88, 31); // Randomly glitch ~10% of scanlines per frame const glitchProbability = 0.1; const maxOffset = intensity; for (let y = 0; y < 31; y++) { if (Math.random() < glitchProbability) { const offset = Math.floor((Math.random() - 0.5) * maxOffset * 2); shiftScanline(imageData, y, offset); } } context.putImageData(imageData, 0, 0); } // Shift a horizontal scanline by offset pixels (with wrapping) function shiftScanline(imageData, y, offset) { const width = imageData.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]; } } // Apply scanline effect (CRT-style horizontal lines) function applyScanlineEffect(context, animState) { const intensity = parseFloat(controls.scanlineIntensity.value); const speed = parseFloat(controls.scanlineSpeed.value); // Create overlay with scanlines context.globalCompositeOperation = "multiply"; context.fillStyle = "rgba(0, 0, 0, " + intensity + ")"; // Animate scanline position const offset = (animState.progress * speed * 31) % 2; for (let y = offset; y < 31; y += 2) { context.fillRect(0, Math.floor(y), 88, 1); } context.globalCompositeOperation = "source-over"; } // Apply RGB split/chromatic aberration effect function applyRgbSplitEffect(context, animState) { const intensity = parseFloat(controls.rgbSplitIntensity.value); const imageData = context.getImageData(0, 0, 88, 31); const result = context.createImageData(88, 31); // Oscillating offset const phase = Math.sin(animState.getPhase(1.0)); const offsetX = Math.round(phase * intensity); for (let y = 0; y < 31; y++) { for (let x = 0; x < 88; x++) { const idx = (y * 88 + x) * 4; // Red channel - shift left const redX = Math.max(0, Math.min(87, x - offsetX)); const redIdx = (y * 88 + 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(87, x + offsetX)); const blueIdx = (y * 88 + 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); } // Apply noise/static effect function applyNoiseEffect(context, animState) { const intensity = parseFloat(controls.noiseIntensity.value); const imageData = context.getImageData(0, 0, 88, 31); 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); } // Animated preview state let previewAnimationId = null; // Update preview (static or animated based on settings) function updatePreview() { if (hasAnimationsEnabled()) { startAnimatedPreview(); } else { stopAnimatedPreview(); drawToContext(ctx); } updateDownloadButtonLabel(); } // Start animated preview loop function startAnimatedPreview() { stopAnimatedPreview(); // Clear any existing let frameNum = 0; let lastFrameTime = performance.now(); const frameDelay = 1000 / ANIMATION_CONFIG.fps; // ms per frame const animate = (currentTime) => { const elapsed = currentTime - lastFrameTime; // Only advance frame if enough time has passed if (elapsed >= frameDelay) { const animState = new AnimationState( frameNum, ANIMATION_CONFIG.totalFrames, ); drawToContext(ctx, animState); frameNum = (frameNum + 1) % ANIMATION_CONFIG.totalFrames; lastFrameTime = currentTime - (elapsed % frameDelay); // Carry over extra time } previewAnimationId = requestAnimationFrame(animate); }; previewAnimationId = requestAnimationFrame(animate); } // Stop animated preview function stopAnimatedPreview() { if (previewAnimationId) { cancelAnimationFrame(previewAnimationId); previewAnimationId = null; } } // Main draw function function drawButton() { updatePreview(); } // Check if any animations are enabled function hasAnimationsEnabled() { return !!( controls.animateTextWave?.checked || controls.animateTextWave2?.checked || controls.animateBgRainbow?.checked || controls.animateBgRainbowGradient?.checked || controls.animateTextRainbow?.checked || controls.animateTextRainbow2?.checked || controls.animateGlitch?.checked || controls.animatePulse?.checked || controls.animateShimmer?.checked || controls.animateScanline?.checked || controls.animateRgbSplit?.checked || controls.animateNoise?.checked || controls.animateRotate?.checked ); } // Update download button label function updateDownloadButtonLabel() { const btn = document.getElementById("download-button"); btn.textContent = "Download GIF"; } // Export as animated GIF async function exportAsGif() { const downloadBtn = document.getElementById("download-button"); const originalText = downloadBtn.textContent; downloadBtn.disabled = true; downloadBtn.textContent = "Generating GIF..."; try { // Create temporary canvas for frame generation const frameCanvas = document.createElement("canvas"); frameCanvas.width = 88; frameCanvas.height = 31; const frameCtx = frameCanvas.getContext("2d"); // Initialize gif.js const gif = new GIF({ workers: 2, quality: 10, workerScript: "/js/gif.worker.js", width: 88, height: 31, }); // Generate frames const totalFrames = ANIMATION_CONFIG.totalFrames; for (let i = 0; i < totalFrames; i++) { const animState = new AnimationState(i, totalFrames); drawToContext(frameCtx, animState); // Add frame to GIF (delay in ms) gif.addFrame(frameCtx, { delay: 1000 / ANIMATION_CONFIG.fps, copy: true, }); // Update progress const progress = Math.round((i / totalFrames) * 100); downloadBtn.textContent = `Generating: ${progress}%`; // Yield to browser to keep UI responsive if (i % 5 === 0) { await new Promise((resolve) => setTimeout(resolve, 0)); } } // Render GIF gif.on("finished", (blob) => { // Download const link = document.createElement("a"); link.download = "button-88x31.gif"; link.href = URL.createObjectURL(blob); link.click(); // Cleanup URL.revokeObjectURL(link.href); downloadBtn.disabled = false; downloadBtn.textContent = originalText; }); gif.on("progress", (progress) => { const percent = Math.round(progress * 100); downloadBtn.textContent = `Encoding: ${percent}%`; }); gif.render(); } catch (error) { console.error("Error generating GIF:", error); downloadBtn.disabled = false; downloadBtn.textContent = originalText; alert("Error generating GIF. Please try again."); } } // Download function document .getElementById("download-button") .addEventListener("click", async () => { await exportAsGif(); }); // Preset buttons document.getElementById("preset-random").addEventListener("click", () => { const randomColor = () => "#" + Math.floor(Math.random() * 16777215) .toString(16) .padStart(6, "0"); // Random background controls.bgType.value = ["solid", "gradient", "texture"][ Math.floor(Math.random() * 3) ]; controls.bgColor.value = randomColor(); controls.gradientColor1.value = randomColor(); controls.gradientColor2.value = randomColor(); controls.gradientAngle.value = Math.floor(Math.random() * 360); controls.textureColor1.value = randomColor(); controls.textureColor2.value = randomColor(); controls.textureType.value = [ "dots", "grid", "diagonal", "checkerboard", "noise", "stars", ][Math.floor(Math.random() * 6)]; // Random text 1 color (50% chance of gradient) controls.textColorType.value = Math.random() > 0.5 ? "gradient" : "solid"; controls.textColor.value = randomColor(); controls.textGradientColor1.value = randomColor(); controls.textGradientColor2.value = randomColor(); controls.textGradientAngle.value = Math.floor(Math.random() * 360); // Random text 2 color (50% chance of gradient) controls.text2ColorType.value = Math.random() > 0.5 ? "gradient" : "solid"; controls.text2Color.value = randomColor(); controls.text2GradientColor1.value = randomColor(); controls.text2GradientColor2.value = randomColor(); controls.text2GradientAngle.value = Math.floor(Math.random() * 360); // Random border controls.borderColor.value = randomColor(); controls.borderWidth.value = Math.floor(Math.random() * 6); controls.borderStyle.value = ["solid", "inset", "outset", "ridge"][ Math.floor(Math.random() * 4) ]; // Random text styles controls.fontBold.checked = Math.random() > 0.5; controls.fontItalic.checked = Math.random() > 0.5; controls.fontBold2.checked = Math.random() > 0.5; controls.fontItalic2.checked = Math.random() > 0.5; // Update displays updateValueDisplay("gradient-angle", controls.gradientAngle.value); updateValueDisplay( "text-gradient-angle", controls.textGradientAngle.value, ); updateValueDisplay( "text2-gradient-angle", controls.text2GradientAngle.value, ); updateValueDisplay("border-width", controls.borderWidth.value); controls.bgType.dispatchEvent(new Event("change")); controls.textColorType.dispatchEvent(new Event("change")); controls.text2ColorType.dispatchEvent(new Event("change")); drawButton(); }); document.getElementById("preset-classic").addEventListener("click", () => { // Classic 90s web button style controls.bgType.value = "gradient"; controls.gradientColor1.value = "#6e6e6eff"; controls.gradientColor2.value = "#979797"; controls.gradientAngle.value = 90; controls.textColorType.value = "solid"; controls.textColor.value = "#000000"; controls.text2ColorType.value = "solid"; controls.text2Color.value = "#000"; controls.borderWidth.value = 2; controls.borderColor.value = "#000000"; controls.borderStyle.value = "outset"; controls.fontFamily.value = "VT323"; controls.fontFamily2.value = "VT323"; controls.fontBold.checked = false; controls.fontBold2.checked = false; controls.fontItalic.checked = false; controls.fontItalic2.checked = false; //controls.text.value = "RITUAL.SH"; //controls.text2.value = "FREE THE WEB"; controls.textEnabled.checked = true; controls.text2Enabled.checked = true; controls.fontSize.value = 12; controls.fontSize2.value = 8; controls.textY.value = 50; controls.text2Y.value = 65; updateValueDisplay("font-size", 12); updateValueDisplay("font-size2", 8); updateValueDisplay("gradient-angle", 90); updateValueDisplay("text-y", 35); updateValueDisplay("text2-y", 65); controls.bgType.dispatchEvent(new Event("change")); controls.textColorType.dispatchEvent(new Event("change")); controls.text2ColorType.dispatchEvent(new Event("change")); drawButton(); }); document.getElementById("preset-modern").addEventListener("click", () => { controls.bgType.value = "gradient"; controls.gradientColor1.value = "#0a0a0a"; controls.gradientColor2.value = "#1a0a2e"; controls.gradientAngle.value = 135; controls.textColorType.value = "gradient"; controls.textGradientColor1.value = "#00ffaa"; controls.textGradientColor2.value = "#00ffff"; controls.textGradientAngle.value = 90; controls.text2ColorType.value = "gradient"; controls.text2GradientColor1.value = "#ff00ff"; controls.text2GradientColor2.value = "#ff6600"; controls.text2GradientAngle.value = 0; controls.borderWidth.value = 1; controls.borderColor.value = "#00ffaa"; controls.borderStyle.value = "solid"; controls.fontFamily.value = "Roboto Mono"; controls.fontFamily2.value = "Roboto Mono"; controls.fontBold.checked = true; controls.fontBold2.checked = false; controls.fontItalic.checked = false; controls.fontItalic2.checked = false; //controls.text.value = "RITUAL.SH"; //controls.text2.value = "EST. 2024"; controls.textEnabled.checked = true; controls.text2Enabled.checked = true; controls.fontSize.value = 11; controls.fontSize2.value = 9; controls.textY.value = 35; controls.text2Y.value = 65; updateValueDisplay("font-size", 11); updateValueDisplay("font-size2", 9); updateValueDisplay("gradient-angle", 135); updateValueDisplay("text-gradient-angle", 90); updateValueDisplay("text2-gradient-angle", 0); updateValueDisplay("text-y", 35); updateValueDisplay("text2-y", 65); controls.textColorType.dispatchEvent(new Event("change")); controls.text2ColorType.dispatchEvent(new Event("change")); controls.bgType.dispatchEvent(new Event("change")); drawButton(); }); // Add event listeners to all controls Object.values(controls).forEach((control) => { if (control) { control.addEventListener("input", drawButton); control.addEventListener("change", drawButton); if (control.type === "range") { control.addEventListener("input", (e) => { updateValueDisplay(e.target.id, e.target.value); }); } } }); // Animation control show/hide listeners if (controls.animateTextWave) { controls.animateTextWave.addEventListener("change", () => { document.getElementById("wave-controls").style.display = controls .animateTextWave.checked ? "block" : "none"; }); } if (controls.animateTextWave2) { controls.animateTextWave2.addEventListener("change", () => { document.getElementById("wave-controls2").style.display = controls .animateTextWave2.checked ? "block" : "none"; }); } if (controls.animateBgRainbow) { controls.animateBgRainbow.addEventListener("change", () => { document.getElementById("rainbow-bg-controls").style.display = controls .animateBgRainbow.checked ? "block" : "none"; }); } if (controls.animateTextRainbow) { controls.animateTextRainbow.addEventListener("change", () => { document.getElementById("rainbow-text-controls").style.display = controls.animateTextRainbow.checked ? "block" : "none"; }); } if (controls.animateTextRainbow2) { controls.animateTextRainbow2.addEventListener("change", () => { document.getElementById("rainbow-text2-controls").style.display = controls.animateTextRainbow2.checked ? "block" : "none"; }); } if (controls.animateGlitch) { controls.animateGlitch.addEventListener("change", () => { document.getElementById("glitch-controls").style.display = controls .animateGlitch.checked ? "block" : "none"; }); } if (controls.animatePulse) { controls.animatePulse.addEventListener("change", () => { document.getElementById("pulse-controls").style.display = controls .animatePulse.checked ? "block" : "none"; }); } if (controls.animateScanline) { controls.animateScanline.addEventListener("change", () => { document.getElementById("scanline-controls").style.display = controls .animateScanline.checked ? "block" : "none"; }); } if (controls.animateRgbSplit) { controls.animateRgbSplit.addEventListener("change", () => { document.getElementById("rgb-split-controls").style.display = controls .animateRgbSplit.checked ? "block" : "none"; }); } if (controls.animateNoise) { controls.animateNoise.addEventListener("change", () => { document.getElementById("noise-controls").style.display = controls .animateNoise.checked ? "block" : "none"; }); } if (controls.animateRotate) { controls.animateRotate.addEventListener("change", () => { document.getElementById("rotate-controls").style.display = controls .animateRotate.checked ? "block" : "none"; }); } // Initial draw drawButton(); } // end setupButtonGenerator })();