diff --git a/assets/js/button-generator.js b/assets/js/button-generator.js deleted file mode 100644 index f33b460..0000000 --- a/assets/js/button-generator.js +++ /dev/null @@ -1,1299 +0,0 @@ -(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 -})(); diff --git a/assets/sass/pages/button-generator.scss b/assets/sass/pages/button-generator.scss index 17c0710..46d2af6 100644 --- a/assets/sass/pages/button-generator.scss +++ b/assets/sass/pages/button-generator.scss @@ -330,6 +330,75 @@ cursor: pointer; accent-color: #0096ff; } + + // Custom file input styling + input[type="file"] { + width: 100%; + padding: 0.75rem 1rem; + border: 1px solid rgba(0, 150, 255, 0.4); + border-radius: 4px; + font-size: 0.9rem; + background: rgba(5, 15, 30, 0.7); + color: rgba(200, 220, 255, 0.95); + transition: all 0.3s ease; + box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.3); + cursor: pointer; + + &::file-selector-button { + padding: 0.5rem 1rem; + margin-right: 1rem; + border: 1px solid rgba(0, 150, 255, 0.5); + border-radius: 4px; + background: linear-gradient( + 135deg, + rgba(0, 120, 200, 0.3) 0%, + rgba(0, 100, 180, 0.2) 100% + ); + color: #66ccff; + cursor: pointer; + font-size: 0.85rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.08em; + transition: all 0.3s ease; + + &:hover { + background: linear-gradient( + 135deg, + rgba(0, 150, 255, 0.4) 0%, + rgba(255, 100, 0, 0.3) 100% + ); + border-color: rgba(0, 150, 255, 0.8); + box-shadow: + 0 0 15px rgba(0, 150, 255, 0.4), + 0 0 20px rgba(255, 120, 0, 0.2); + color: #fff; + text-shadow: 0 0 10px rgba(0, 150, 255, 0.6); + transform: translateY(-1px); + } + + &:active { + transform: translateY(0); + } + } + + &:hover { + border-color: rgba(0, 150, 255, 0.7); + box-shadow: + 0 0 15px rgba(0, 150, 255, 0.3), + inset 0 2px 8px rgba(0, 0, 0, 0.4); + background: rgba(5, 15, 30, 0.9); + } + + &:focus { + outline: none; + border-color: rgba(0, 150, 255, 0.7); + box-shadow: + 0 0 15px rgba(0, 150, 255, 0.3), + inset 0 2px 8px rgba(0, 0, 0, 0.4); + background: rgba(5, 15, 30, 0.9); + } + } } .control-group-header { diff --git a/static/js/button-generator/button-generator-core.js b/static/js/button-generator/button-generator-core.js index 1c6126a..fbca8eb 100644 --- a/static/js/button-generator/button-generator-core.js +++ b/static/js/button-generator/button-generator-core.js @@ -149,6 +149,18 @@ export class ButtonGenerator { values[id] = element.checked; } else if (element.type === 'range' || element.type === 'number') { values[id] = parseFloat(element.value); + } else if (element.type === 'file') { + // For file inputs, return an object with file metadata and blob URL + if (element.dataset.blobUrl) { + values[id] = { + fileName: element.dataset.fileName, + blobUrl: element.dataset.blobUrl, + fileSize: parseInt(element.dataset.fileSize), + fileType: element.dataset.fileType + }; + } else { + values[id] = null; + } } else { values[id] = element.value; } diff --git a/static/js/button-generator/effects/background-external-image.js b/static/js/button-generator/effects/background-external-image.js index 1e9fbf6..d3ffc9d 100644 --- a/static/js/button-generator/effects/background-external-image.js +++ b/static/js/button-generator/effects/background-external-image.js @@ -37,12 +37,13 @@ export class ExternalImageBackgroundEffect extends ButtonEffect { defineControls() { return [ { - id: 'bg-image-url', - type: 'text', - label: 'Image URL', + id: 'bg-image-file', + type: 'file', + label: 'Image File', defaultValue: '', + accept: 'image/*', showWhen: 'bg-type', - description: 'URL of the image to use as background' + description: 'Select an image file from your computer' }, { id: 'bg-image-fit', @@ -53,11 +54,45 @@ export class ExternalImageBackgroundEffect extends ButtonEffect { { value: 'cover', label: 'Cover (fill, crop if needed)' }, { value: 'contain', label: 'Contain (fit inside)' }, { value: 'stretch', label: 'Stretch (may distort)' }, - { value: 'center', label: 'Center (actual size)' } + { value: 'center', label: 'Center (actual size)' }, + { value: 'manual', label: 'Manual (custom zoom & position)' } ], showWhen: 'bg-type', description: 'How the image should fit in the canvas' }, + { + id: 'bg-image-zoom', + type: 'range', + label: 'Image Zoom', + defaultValue: 100, + min: 10, + max: 500, + step: 5, + showWhen: 'bg-image-fit', + description: 'Zoom level for manual positioning (percentage)' + }, + { + id: 'bg-image-offset-x', + type: 'range', + label: 'Horizontal Position', + defaultValue: 50, + min: 0, + max: 100, + step: 1, + showWhen: 'bg-image-fit', + description: 'Horizontal position of the image (percentage)' + }, + { + id: 'bg-image-offset-y', + type: 'range', + label: 'Vertical Position', + defaultValue: 50, + min: 0, + max: 100, + step: 1, + showWhen: 'bg-image-fit', + description: 'Vertical position of the image (percentage)' + }, { id: 'bg-image-opacity', type: 'range', @@ -77,24 +112,24 @@ export class ExternalImageBackgroundEffect extends ButtonEffect { } /** - * Start loading an image from a URL with caching + * Start loading an image from a blob URL with caching * Triggers async loading but returns immediately - * @param {string} url - Image URL + * @param {string} blobUrl - Blob URL created from uploaded file */ - startLoadingImage(url) { + startLoadingImage(blobUrl) { // Skip if already cached or loading - if (this.imageCache.has(url) || this.loadingImages.has(url)) { + if (this.imageCache.has(blobUrl) || this.loadingImages.has(blobUrl)) { return; } // Mark as loading to prevent duplicate requests - this.loadingImages.set(url, true); + this.loadingImages.set(blobUrl, true); const img = new Image(); img.onload = () => { - this.imageCache.set(url, img); - this.loadingImages.delete(url); + this.imageCache.set(blobUrl, img); + this.loadingImages.delete(blobUrl); // Trigger a redraw when image loads const event = new CustomEvent('imageLoaded'); window.dispatchEvent(event); @@ -102,29 +137,29 @@ export class ExternalImageBackgroundEffect extends ButtonEffect { img.onerror = () => { // Cache the error state to prevent retry spam - this.imageCache.set(url, 'ERROR'); - this.loadingImages.delete(url); + this.imageCache.set(blobUrl, 'ERROR'); + this.loadingImages.delete(blobUrl); // Trigger a redraw to show error state const event = new CustomEvent('imageLoaded'); window.dispatchEvent(event); }; - img.src = url; + img.src = blobUrl; } /** * Get cached image if available - * @param {string} url - Image URL + * @param {string} blobUrl - Blob URL * @returns {HTMLImageElement|string|null} Image element, 'ERROR', or null */ - getCachedImage(url) { - return this.imageCache.get(url) || null; + getCachedImage(blobUrl) { + return this.imageCache.get(blobUrl) || null; } /** * Draw image with the specified fit mode */ - drawImage(context, img, fitMode, opacity, width, height) { + drawImage(context, img, fitMode, opacity, width, height, zoom, offsetX, offsetY) { context.globalAlpha = opacity; const imgRatio = img.width / img.height; @@ -174,6 +209,31 @@ export class ExternalImageBackgroundEffect extends ButtonEffect { drawY = (height - drawHeight) / 2; break; + case 'manual': + // Manual positioning with zoom and offset controls + const zoomFactor = zoom / 100; + + // Start with the larger dimension to ensure coverage + if (imgRatio > canvasRatio) { + // Image is wider relative to canvas + drawHeight = height * zoomFactor; + drawWidth = drawHeight * imgRatio; + } else { + // Image is taller relative to canvas + drawWidth = width * zoomFactor; + drawHeight = drawWidth / imgRatio; + } + + // Calculate the range of movement (how far can we move the image) + const maxOffsetX = drawWidth - width; + const maxOffsetY = drawHeight - height; + + // Convert percentage (0-100) to actual position + // 0% = image fully left/top, 100% = image fully right/bottom + drawX = -(maxOffsetX * offsetX / 100); + drawY = -(maxOffsetY * offsetY / 100); + break; + case 'stretch': // Stretch to fill (default values already set) break; @@ -184,12 +244,15 @@ export class ExternalImageBackgroundEffect extends ButtonEffect { } apply(context, controlValues, animState, renderData) { - const url = controlValues['bg-image-url'] || ''; + const file = controlValues['bg-image-file']; const fitMode = controlValues['bg-image-fit'] || 'cover'; const opacity = controlValues['bg-image-opacity'] ?? 1; + const zoom = controlValues['bg-image-zoom'] ?? 100; + const offsetX = controlValues['bg-image-offset-x'] ?? 50; + const offsetY = controlValues['bg-image-offset-y'] ?? 50; - // If no URL provided, fill with a placeholder color - if (!url.trim()) { + // If no file selected, fill with a placeholder color + if (!file || !file.blobUrl) { context.fillStyle = '#cccccc'; context.fillRect(0, 0, renderData.width, renderData.height); @@ -198,16 +261,16 @@ export class ExternalImageBackgroundEffect extends ButtonEffect { context.font = '8px Arial'; context.textAlign = 'center'; context.textBaseline = 'middle'; - context.fillText('Enter image URL', renderData.centerX, renderData.centerY); + context.fillText('Select an image', renderData.centerX, renderData.centerY); return; } // Start loading if not already cached or loading - const cachedImage = this.getCachedImage(url); + const cachedImage = this.getCachedImage(file.blobUrl); if (!cachedImage) { // Not cached yet - start loading - this.startLoadingImage(url); + this.startLoadingImage(file.blobUrl); // Draw loading state context.fillStyle = '#3498db'; @@ -236,7 +299,7 @@ export class ExternalImageBackgroundEffect extends ButtonEffect { } // Image is loaded - draw it - this.drawImage(context, cachedImage, fitMode, opacity, renderData.width, renderData.height); + this.drawImage(context, cachedImage, fitMode, opacity, renderData.width, renderData.height, zoom, offsetX, offsetY); } } diff --git a/static/js/button-generator/effects/background-solid.js b/static/js/button-generator/effects/background-solid.js index 6ba8155..a835100 100644 --- a/static/js/button-generator/effects/background-solid.js +++ b/static/js/button-generator/effects/background-solid.js @@ -26,7 +26,7 @@ export class SolidBackgroundEffect extends ButtonEffect { { value: "gradient", label: "Gradient" }, { value: "texture", label: "Texture" }, { value: "emoji-wallpaper", label: "Emoji Wallpaper" }, - // { value: 'external-image', label: 'External Image' } + { value: 'external-image', label: 'Image Upload' } ], }, { diff --git a/static/js/button-generator/main.js b/static/js/button-generator/main.js index 71c400e..c6061dc 100644 --- a/static/js/button-generator/main.js +++ b/static/js/button-generator/main.js @@ -89,7 +89,7 @@ async function setupApp() { gradientBg.register(generator); textureBg.register(generator); emojiWallpaper.register(generator); - //externalImage.register(generator); + externalImage.register(generator); rainbowBg.register(generator); rain.register(generator); starfield.register(generator); diff --git a/static/js/button-generator/ui-builder.js b/static/js/button-generator/ui-builder.js index afafbbe..2ab5e1d 100644 --- a/static/js/button-generator/ui-builder.js +++ b/static/js/button-generator/ui-builder.js @@ -261,6 +261,9 @@ export class UIBuilder { case 'text': return this.createTextInput(id, label, defaultValue, showWhen, description); + case 'file': + return this.createFileInput(id, label, defaultValue, showWhen, description, controlDef.accept); + default: console.warn(`Unknown control type: ${type}`); return null; @@ -438,6 +441,57 @@ export class UIBuilder { return container; } + /** + * Create a file input control + */ + createFileInput(id, label, defaultValue, showWhen, description, accept) { + const container = document.createElement('div'); + + const labelEl = document.createElement('label'); + labelEl.htmlFor = id; + labelEl.textContent = label; + + const input = document.createElement('input'); + input.type = 'file'; + input.id = id; + if (accept) { + input.accept = accept; + } + + // Store the file data on the input element + input.addEventListener('change', (e) => { + const file = e.target.files[0]; + if (file) { + // Create a blob URL for the file + const blobUrl = URL.createObjectURL(file); + // Store file metadata on the input element + input.dataset.fileName = file.name; + input.dataset.blobUrl = blobUrl; + input.dataset.fileSize = file.size; + input.dataset.fileType = file.type; + } else { + // Clear the data if no file is selected + delete input.dataset.fileName; + delete input.dataset.blobUrl; + delete input.dataset.fileSize; + delete input.dataset.fileType; + } + }); + + container.appendChild(labelEl); + container.appendChild(input); + + if (showWhen) { + container.style.display = 'none'; + container.dataset.showWhen = showWhen; + } + + // Add tooltip handlers to the label + this.addTooltipHandlers(labelEl, description); + + return container; + } + /** * Setup conditional visibility for controls * Should be called after all controls are created @@ -468,10 +522,16 @@ export class UIBuilder { control.style.display = triggerControl.value === 'texture' ? 'block' : 'none'; } else if (controlId && (controlId.startsWith('emoji-') || controlId === 'emoji-text')) { control.style.display = triggerControl.value === 'emoji-wallpaper' ? 'block' : 'none'; - } else if (controlId && (controlId.startsWith('bg-image-') || controlId === 'bg-image-url' || controlId === 'bg-image-fit' || controlId === 'bg-image-opacity')) { + } else if (controlId && (controlId.startsWith('bg-image-') || controlId === 'bg-image-file' || controlId === 'bg-image-fit' || controlId === 'bg-image-opacity')) { control.style.display = triggerControl.value === 'external-image' ? 'block' : 'none'; } } + // For image fit controls (zoom and position only show when manual mode) + else if (triggerControlId === 'bg-image-fit') { + if (controlId && (controlId === 'bg-image-zoom' || controlId === 'bg-image-offset-x' || controlId === 'bg-image-offset-y')) { + control.style.display = triggerControl.value === 'manual' ? 'block' : 'none'; + } + } // For text color controls else if (triggerControlId === 'text-color-type') { if (controlId === 'text-color') {