diff --git a/assets/js/button-generator.js b/assets/js/button-generator.js index 62acb20..f33b460 100644 --- a/assets/js/button-generator.js +++ b/assets/js/button-generator.js @@ -1,340 +1,483 @@ (function () { - 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(); - }); - - // Collapsible sections functionality - document.querySelectorAll(".control-group-header").forEach((header) => { - header.addEventListener("click", () => { - const controlGroup = header.closest(".control-group"); - controlGroup.classList.toggle("collapsed"); - }); - }); - - // 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"), - }; - - // 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; + // Wait for DOM to be ready + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", init); + } else { + init(); } - // Helper function to draw to a single context - function drawToContext(context) { - context.clearRect(0, 0, 88, 31); + function init() { + setupCollapsible(); + setupButtonGenerator(); + } - // Draw background - if (controls.bgType.value === "solid") { - 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; + // Collapsible sections functionality + function setupCollapsible() { + const headers = document.querySelectorAll(".control-group-header"); + console.log("Found", headers.length, "collapsible headers"); - const gradient = context.createLinearGradient(x1, y1, x2, y2); - 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); - } + 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()); + } + }); + }); + } - // Draw border - const borderWidth = parseFloat(controls.borderWidth.value); - if (borderWidth > 0) { - const style = controls.borderStyle.value; + function setupButtonGenerator() { + const canvas = document.getElementById("button-canvas"); + const ctx = canvas.getContext("2d"); - 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(); + // 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", + ]; - 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, - ); + // 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}"`), + ]); - context.strokeStyle = "#000000"; - context.strokeRect( - (borderWidth * 3) / 4, - (borderWidth * 3) / 4, - 88 - borderWidth * 1.5, - 31 - borderWidth * 1.5, - ); + 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; } } - // Draw text line 1 - const text = controls.text.value; - if (text && controls.textEnabled.checked) { - const fontSize = parseFloat(controls.fontSize.value); - const fontWeight = controls.fontBold.checked ? "bold" : "normal"; - const fontStyle = controls.fontItalic.checked ? "italic" : "normal"; - const fontFamily = controls.fontFamily.value; + // 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 x = (parseFloat(controls.textX.value) / 100) * 88; - const y = (parseFloat(controls.textY.value) / 100) * 31; + const baseX = (parseFloat(controls[`text${prefix}X`].value) / 100) * 88; + const baseY = (parseFloat(controls[`text${prefix}Y`].value) / 100) * 31; - if (controls.textOutline.checked) { - context.strokeStyle = controls.outlineColor.value; + // 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); } - // Apply text color or gradient - if (controls.textColorType.value === "solid") { - context.fillStyle = controls.textColor.value; + 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.textGradientAngle.value) * (Math.PI / 180); + 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; @@ -342,253 +485,815 @@ const y2 = y + fontSize / 2 - (Math.sin(angle) * fontSize) / 2; const textGradient = context.createLinearGradient(x1, y1, x2, y2); - textGradient.addColorStop(0, controls.textGradientColor1.value); - textGradient.addColorStop(1, controls.textGradientColor2.value); - context.fillStyle = textGradient; - } - - context.fillText(text, x, y); - } - - // Draw text line 2 - const text2 = controls.text2.value; - if (text2 && controls.text2Enabled.checked) { - const fontSize2 = parseFloat(controls.fontSize2.value); - const fontWeight2 = controls.fontBold2.checked ? "bold" : "normal"; - const fontStyle2 = controls.fontItalic2.checked ? "italic" : "normal"; - const fontFamily2 = controls.fontFamily2.value; - - context.font = `${fontStyle2} ${fontWeight2} ${fontSize2}px "${fontFamily2}"`; - context.textAlign = "center"; - context.textBaseline = "middle"; - - const x2 = (parseFloat(controls.text2X.value) / 100) * 88; - const y2 = (parseFloat(controls.text2Y.value) / 100) * 31; - - if (controls.text2Outline.checked) { - context.strokeStyle = controls.outline2Color.value; - context.lineWidth = 2; - context.strokeText(text2, x2, y2); - } - - // Apply text color or gradient - if (controls.text2ColorType.value === "solid") { - context.fillStyle = controls.text2Color.value; - } else { - const angle2 = - parseFloat(controls.text2GradientAngle.value) * (Math.PI / 180); - const text2Width = context.measureText(text2).width; - const x1_2 = x2 - text2Width / 2 + (Math.cos(angle2) * text2Width) / 2; - const y1_2 = y2 - fontSize2 / 2 + (Math.sin(angle2) * fontSize2) / 2; - const x2_2 = x2 + text2Width / 2 - (Math.cos(angle2) * text2Width) / 2; - const y2_2 = y2 + fontSize2 / 2 - (Math.sin(angle2) * fontSize2) / 2; - - const text2Gradient = context.createLinearGradient( - x1_2, - y1_2, - x2_2, - y2_2, + textGradient.addColorStop( + 0, + controls[`text${prefix}GradientColor1`].value, ); - text2Gradient.addColorStop(0, controls.text2GradientColor1.value); - text2Gradient.addColorStop(1, controls.text2GradientColor2.value); - context.fillStyle = text2Gradient; + textGradient.addColorStop( + 1, + controls[`text${prefix}GradientColor2`].value, + ); + fillStyle = textGradient; + strokeStyle = controls[`outline${prefix}Color`].value; } - context.fillText(text2, x2, y2); + return { fillStyle, strokeStyle }; } - } - // Main draw function - function drawButton() { - drawToContext(ctx); - } + // Helper function to draw to a single context + function drawToContext(context, animState = null) { + context.clearRect(0, 0, 88, 31); - // Download function - document.getElementById("download-button").addEventListener("click", () => { - const link = document.createElement("a"); - link.download = "button-88x31.png"; - link.href = canvas.toDataURL(); - link.click(); - }); + // 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); - // Preset buttons - document.getElementById("preset-random").addEventListener("click", () => { - const randomColor = () => - "#" + - Math.floor(Math.random() * 16777215) - .toString(16) - .padStart(6, "0"); + context.save(); + context.translate(44, 15.5); + context.rotate(angle); + context.translate(-44, -15.5); + } - // 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)]; + // 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; - // 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); + const gradient = context.createLinearGradient(x1, y1, x2, y2); - // 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); + // 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); + } - // 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) - ]; + 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); + } - // 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; + // Rainbow gradient overlay (travels across background) + if (animState && controls.animateBgRainbowGradient?.checked) { + // Map progress to position (-100 to 100) + const position = animState.progress * 200 - 100; - // 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); + // Create a horizontal gradient that sweeps across + const rainbowGradient = context.createLinearGradient( + position - 50, + 0, + position + 50, + 0, + ); - controls.bgType.dispatchEvent(new Event("change")); - controls.textColorType.dispatchEvent(new Event("change")); - controls.text2ColorType.dispatchEvent(new Event("change")); - drawButton(); - }); + // 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)`, + ); - document.getElementById("preset-classic").addEventListener("click", () => { - // Classic 90s web button style - controls.bgType.value = "gradient"; - controls.gradientColor1.value = "#0066cc"; - controls.gradientColor2.value = "#0099ff"; - controls.gradientAngle.value = 90; + context.fillStyle = rainbowGradient; + context.fillRect(0, 0, 88, 31); + } - controls.textColorType.value = "solid"; - controls.textColor.value = "#ffffff"; - controls.text2ColorType.value = "solid"; - controls.text2Color.value = "#ffffff"; + // Draw border + const borderWidth = parseFloat(controls.borderWidth.value); + if (borderWidth > 0) { + const style = controls.borderStyle.value; - controls.borderWidth.value = 2; - controls.borderColor.value = "#000000"; - controls.borderStyle.value = "outset"; + 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(); - controls.fontFamily.value = "Oswald"; - controls.fontFamily2.value = "Lato"; - controls.fontBold.checked = true; - controls.fontBold2.checked = false; - controls.fontItalic.checked = false; - controls.fontItalic2.checked = false; + 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, + ); - 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 = 35; - controls.text2Y.value = 65; + context.strokeStyle = "#000000"; + context.strokeRect( + (borderWidth * 3) / 4, + (borderWidth * 3) / 4, + 88 - borderWidth * 1.5, + 31 - borderWidth * 1.5, + ); + } + } - updateValueDisplay("font-size", 12); - updateValueDisplay("font-size2", 8); - updateValueDisplay("gradient-angle", 90); - updateValueDisplay("text-y", 35); - updateValueDisplay("text2-y", 65); + // 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); - controls.bgType.dispatchEvent(new Event("change")); - controls.textColorType.dispatchEvent(new Event("change")); - controls.text2ColorType.dispatchEvent(new Event("change")); - drawButton(); - }); + context.save(); + context.translate(44, 15.5); + context.scale(scale, scale); + context.translate(-44, -15.5); + } - document.getElementById("preset-modern").addEventListener("click", () => { - // Modern cyberpunk style with gradient text - controls.bgType.value = "gradient"; - controls.gradientColor1.value = "#0a0a0a"; - controls.gradientColor2.value = "#1a0a2e"; - controls.gradientAngle.value = 135; + // Draw text line 1 + drawTextLine(context, 1, animState); - controls.textColorType.value = "gradient"; - controls.textGradientColor1.value = "#00ffaa"; - controls.textGradientColor2.value = "#00ffff"; - controls.textGradientAngle.value = 90; + // Draw text line 2 + drawTextLine(context, 2, animState); - controls.text2ColorType.value = "gradient"; - controls.text2GradientColor1.value = "#ff00ff"; - controls.text2GradientColor2.value = "#ff6600"; - controls.text2GradientAngle.value = 0; + // Restore context if pulse was applied + if (animState && controls.animatePulse?.checked) { + context.restore(); + } - controls.borderWidth.value = 1; - controls.borderColor.value = "#00ffaa"; - controls.borderStyle.value = "solid"; + // Apply shimmer effect + if (animState && controls.animateShimmer?.checked) { + const shimmerX = animState.progress * 120 - 20; // Sweep from -20 to 100 - 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; + 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)"); - 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; + context.fillStyle = shimmerGradient; + context.fillRect(0, 0, 88, 31); + } - 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); + // Apply glitch effect + if (animState && controls.animateGlitch?.checked) { + applyGlitchEffect(context, animState); + } - controls.textColorType.dispatchEvent(new Event("change")); - controls.text2ColorType.dispatchEvent(new Event("change")); - controls.bgType.dispatchEvent(new Event("change")); - drawButton(); - }); + // Apply scanline effect + if (animState && controls.animateScanline?.checked) { + applyScanlineEffect(context, animState); + } - // Add event listeners to all controls - Object.values(controls).forEach((control) => { - if (control) { - control.addEventListener("input", drawButton); - control.addEventListener("change", drawButton); + // Apply RGB split effect + if (animState && controls.animateRgbSplit?.checked) { + applyRgbSplitEffect(context, animState); + } - if (control.type === "range") { - control.addEventListener("input", (e) => { - updateValueDisplay(e.target.id, e.target.value); + // 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."); } } - }); - // Initial draw - drawButton(); + // 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 a6a17ec..c6aac4b 100644 --- a/assets/sass/pages/button-generator.scss +++ b/assets/sass/pages/button-generator.scss @@ -143,7 +143,7 @@ image-rendering: pixelated; image-rendering: -moz-crisp-edges; image-rendering: crisp-edges; - border: 1px solid rgba(0, 150, 255, 0.4); + //border: 1px solid rgba(0, 150, 255, 0.4); box-shadow: 0 0 15px rgba(0, 150, 255, 0.2); } @@ -159,7 +159,7 @@ rgba(0, 100, 180, 0.15) 0%, rgba(0, 80, 150, 0.1) 100% ); - padding: 1.5rem; + padding: 1rem; border-radius: 6px; border: 1px solid rgba(0, 150, 255, 0.3); box-shadow: @@ -358,23 +358,48 @@ font-weight: bold; transition: transform 0.3s ease; color: rgba(0, 150, 255, 0.8); + } - .control-group.collapsed & { - transform: rotate(180deg); - } + .control-group.collapsed .toggle-icon { + transform: rotate(180deg); } .control-group-content { overflow: hidden; + max-height: 2000px; transition: max-height 0.3s ease, opacity 0.3s ease; + } - .control-group.collapsed & { - max-height: 0; - opacity: 0; - pointer-events: none; - } + .control-group.collapsed .control-group-content { + max-height: 0; + opacity: 0; + pointer-events: none; + } + + .info-text { + font-size: 0.85rem; + color: rgba(150, 200, 255, 0.7); + font-style: italic; + margin-bottom: 1rem; + padding: 0.5rem; + background: rgba(0, 100, 180, 0.1); + border-left: 3px solid rgba(0, 150, 255, 0.5); + border-radius: 2px; + } + + h4 { + margin-top: 1.5rem; + margin-bottom: 0.75rem; + color: rgba(100, 180, 255, 0.85); + font-family: Verdana, "Trebuchet MS", Tahoma, sans-serif; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.08em; + font-size: 0.9rem; + padding-bottom: 0.5rem; + border-bottom: 1px solid rgba(0, 150, 255, 0.3); } .checkbox-row { diff --git a/assets/sass/pages/resources.scss b/assets/sass/pages/resources.scss index f70ee2c..5355ccb 100644 --- a/assets/sass/pages/resources.scss +++ b/assets/sass/pages/resources.scss @@ -703,26 +703,26 @@ &::before { content: ""; position: absolute; - left: -4rem; + left: -2rem; top: 50%; transform: translateY(-50%); width: 3rem; height: 3px; background: linear-gradient(90deg, transparent, #0096ff); - box-shadow: 0 0 10px rgba(0, 150, 255, 0.8); + //box-shadow: 0 0 10px rgba(0, 150, 255, 0.8); } // Orange line on the right &::after { content: ""; position: absolute; - right: -4rem; + right: -2rem; top: 50%; transform: translateY(-50%); width: 3rem; height: 3px; background: linear-gradient(90deg, #ff7800, transparent); - box-shadow: 0 0 10px rgba(255, 120, 0, 0.8); + //box-shadow: 0 0 10px rgba(255, 120, 0, 0.8); } @include media-up(md) { @@ -730,15 +730,15 @@ &::before, &::after { - width: 5rem; + width: 6rem; } &::before { - left: -6rem; + left: -2rem; } &::after { - right: -6rem; + right: -2rem; } } } diff --git a/content/media/taskmaster-champion-of-champions-4/canvas.png b/content/media/taskmaster-champion-of-champions-4/canvas.png new file mode 100644 index 0000000..a96b8e7 Binary files /dev/null and b/content/media/taskmaster-champion-of-champions-4/canvas.png differ diff --git a/content/resources/button-generator/index.md b/content/resources/button-generator/index.md index 0dbad1b..fad3797 100644 --- a/content/resources/button-generator/index.md +++ b/content/resources/button-generator/index.md @@ -10,7 +10,7 @@ draft: false Welcome to my 88x31 button creator, this is a pretty rough and ready implementation so it could be buggy, please let me know if you find any issues. -Currently this only supports static images and exports as png due to the basic `canvas` tag limitations. I have approximate plans for how to make this export gifs and potentially make animated buttons, please look forward to it. +This supports gif despite the basic `canvas` tag limitation courtesy of [gif.js](https://github.com/jnordberg/gif.js) - none of this would be possible without that project. Big thanks to [neonaut's 88x31 archive](https://neonaut.neocities.org/cyber/88x31) and everyone who made the buttons that appear there. You should check it out if you need inspiration for your button! diff --git a/layouts/_default/baseof.html b/layouts/_default/baseof.html index e3146ce..0ac2b0f 100755 --- a/layouts/_default/baseof.html +++ b/layouts/_default/baseof.html @@ -36,6 +36,7 @@ {{ if or (findRE "{{<\\s*button-generator" .RawContent) (findRE "{{%\\s*button-generator" .RawContent) }} + {{ $buttonGenerator := resources.Get "js/button-generator.js" | resources.Minify | resources.Fingerprint }} {{ end }} diff --git a/layouts/shortcodes/button-generator.html b/layouts/shortcodes/button-generator.html index 0572f2f..8d65fa3 100644 --- a/layouts/shortcodes/button-generator.html +++ b/layouts/shortcodes/button-generator.html @@ -121,6 +121,62 @@ +
+

+ Text Line 1 Animation + +

+
+ + + + + +
+
+

Text Line 2 @@ -219,6 +275,62 @@

+
+

+ Text Line 2 Animation + +

+
+ + + + + +
+
+

Background @@ -287,6 +399,37 @@

+
+

+ Background Animation + +

+
+ + + + +
+
+

Border @@ -310,6 +453,155 @@

+ +
+

+ Special Effects + +

+
+

+ Almost all animations should stack, so pick as many as you want. +

+ + + + + + + + + + + + + + + + + + {{/* + + + */}} +
+
diff --git a/static/js/gif.js b/static/js/gif.js new file mode 100644 index 0000000..2e4d204 --- /dev/null +++ b/static/js/gif.js @@ -0,0 +1,3 @@ +// gif.js 0.2.0 - https://github.com/jnordberg/gif.js +(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.GIF=f()}})(function(){var define,module,exports;return function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o0&&this._events[type].length>m){this._events[type].warned=true;console.error("(node) warning: possible EventEmitter memory "+"leak detected. %d listeners added. "+"Use emitter.setMaxListeners() to increase limit.",this._events[type].length);if(typeof console.trace==="function"){console.trace()}}}return this};EventEmitter.prototype.on=EventEmitter.prototype.addListener;EventEmitter.prototype.once=function(type,listener){if(!isFunction(listener))throw TypeError("listener must be a function");var fired=false;function g(){this.removeListener(type,g);if(!fired){fired=true;listener.apply(this,arguments)}}g.listener=listener;this.on(type,g);return this};EventEmitter.prototype.removeListener=function(type,listener){var list,position,length,i;if(!isFunction(listener))throw TypeError("listener must be a function");if(!this._events||!this._events[type])return this;list=this._events[type];length=list.length;position=-1;if(list===listener||isFunction(list.listener)&&list.listener===listener){delete this._events[type];if(this._events.removeListener)this.emit("removeListener",type,listener)}else if(isObject(list)){for(i=length;i-- >0;){if(list[i]===listener||list[i].listener&&list[i].listener===listener){position=i;break}}if(position<0)return this;if(list.length===1){list.length=0;delete this._events[type]}else{list.splice(position,1)}if(this._events.removeListener)this.emit("removeListener",type,listener)}return this};EventEmitter.prototype.removeAllListeners=function(type){var key,listeners;if(!this._events)return this;if(!this._events.removeListener){if(arguments.length===0)this._events={};else if(this._events[type])delete this._events[type];return this}if(arguments.length===0){for(key in this._events){if(key==="removeListener")continue;this.removeAllListeners(key)}this.removeAllListeners("removeListener");this._events={};return this}listeners=this._events[type];if(isFunction(listeners)){this.removeListener(type,listeners)}else if(listeners){while(listeners.length)this.removeListener(type,listeners[listeners.length-1])}delete this._events[type];return this};EventEmitter.prototype.listeners=function(type){var ret;if(!this._events||!this._events[type])ret=[];else if(isFunction(this._events[type]))ret=[this._events[type]];else ret=this._events[type].slice();return ret};EventEmitter.prototype.listenerCount=function(type){if(this._events){var evlistener=this._events[type];if(isFunction(evlistener))return 1;else if(evlistener)return evlistener.length}return 0};EventEmitter.listenerCount=function(emitter,type){return emitter.listenerCount(type)};function isFunction(arg){return typeof arg==="function"}function isNumber(arg){return typeof arg==="number"}function isObject(arg){return typeof arg==="object"&&arg!==null}function isUndefined(arg){return arg===void 0}},{}],2:[function(require,module,exports){var UA,browser,mode,platform,ua;ua=navigator.userAgent.toLowerCase();platform=navigator.platform.toLowerCase();UA=ua.match(/(opera|ie|firefox|chrome|version)[\s\/:]([\w\d\.]+)?.*?(safari|version[\s\/:]([\w\d\.]+)|$)/)||[null,"unknown",0];mode=UA[1]==="ie"&&document.documentMode;browser={name:UA[1]==="version"?UA[3]:UA[1],version:mode||parseFloat(UA[1]==="opera"&&UA[4]?UA[4]:UA[2]),platform:{name:ua.match(/ip(?:ad|od|hone)/)?"ios":(ua.match(/(?:webos|android)/)||platform.match(/mac|win|linux/)||["other"])[0]}};browser[browser.name]=true;browser[browser.name+parseInt(browser.version,10)]=true;browser.platform[browser.platform.name]=true;module.exports=browser},{}],3:[function(require,module,exports){var EventEmitter,GIF,browser,extend=function(child,parent){for(var key in parent){if(hasProp.call(parent,key))child[key]=parent[key]}function ctor(){this.constructor=child}ctor.prototype=parent.prototype;child.prototype=new ctor;child.__super__=parent.prototype;return child},hasProp={}.hasOwnProperty,indexOf=[].indexOf||function(item){for(var i=0,l=this.length;iref;i=0<=ref?++j:--j){results.push(null)}return results}.call(this);numWorkers=this.spawnWorkers();if(this.options.globalPalette===true){this.renderNextFrame()}else{for(i=j=0,ref=numWorkers;0<=ref?jref;i=0<=ref?++j:--j){this.renderNextFrame()}}this.emit("start");return this.emit("progress",0)};GIF.prototype.abort=function(){var worker;while(true){worker=this.activeWorkers.shift();if(worker==null){break}this.log("killing active worker");worker.terminate()}this.running=false;return this.emit("abort")};GIF.prototype.spawnWorkers=function(){var j,numWorkers,ref,results;numWorkers=Math.min(this.options.workers,this.frames.length);(function(){results=[];for(var j=ref=this.freeWorkers.length;ref<=numWorkers?jnumWorkers;ref<=numWorkers?j++:j--){results.push(j)}return results}).apply(this).forEach(function(_this){return function(i){var worker;_this.log("spawning worker "+i);worker=new Worker(_this.options.workerScript);worker.onmessage=function(event){_this.activeWorkers.splice(_this.activeWorkers.indexOf(worker),1);_this.freeWorkers.push(worker);return _this.frameFinished(event.data)};return _this.freeWorkers.push(worker)}}(this));return numWorkers};GIF.prototype.frameFinished=function(frame){var i,j,ref;this.log("frame "+frame.index+" finished - "+this.activeWorkers.length+" active");this.finishedFrames++;this.emit("progress",this.finishedFrames/this.frames.length);this.imageParts[frame.index]=frame;if(this.options.globalPalette===true){this.options.globalPalette=frame.globalPalette;this.log("global palette analyzed");if(this.frames.length>2){for(i=j=1,ref=this.freeWorkers.length;1<=ref?jref;i=1<=ref?++j:--j){this.renderNextFrame()}}}if(indexOf.call(this.imageParts,null)>=0){return this.renderNextFrame()}else{return this.finishRendering()}};GIF.prototype.finishRendering=function(){var data,frame,i,image,j,k,l,len,len1,len2,len3,offset,page,ref,ref1,ref2;len=0;ref=this.imageParts;for(j=0,len1=ref.length;j=this.frames.length){return}frame=this.frames[this.nextFrame++];worker=this.freeWorkers.shift();task=this.getTask(frame);this.log("starting frame "+(task.index+1)+" of "+this.frames.length);this.activeWorkers.push(worker);return worker.postMessage(task)};GIF.prototype.getContextData=function(ctx){return ctx.getImageData(0,0,this.options.width,this.options.height).data};GIF.prototype.getImageData=function(image){var ctx;if(this._canvas==null){this._canvas=document.createElement("canvas");this._canvas.width=this.options.width;this._canvas.height=this.options.height}ctx=this._canvas.getContext("2d");ctx.setFill=this.options.background;ctx.fillRect(0,0,this.options.width,this.options.height);ctx.drawImage(image,0,0);return this.getContextData(ctx)};GIF.prototype.getTask=function(frame){var index,task;index=this.frames.indexOf(frame);task={index:index,last:index===this.frames.length-1,delay:frame.delay,transparent:frame.transparent,width:this.options.width,height:this.options.height,quality:this.options.quality,dither:this.options.dither,globalPalette:this.options.globalPalette,repeat:this.options.repeat,canTransfer:browser.name==="chrome"};if(frame.data!=null){task.data=frame.data}else if(frame.context!=null){task.data=this.getContextData(frame.context)}else if(frame.image!=null){task.data=this.getImageData(frame.image)}else{throw new Error("Invalid frame")}return task};GIF.prototype.log=function(){var args;args=1<=arguments.length?slice.call(arguments,0):[];if(!this.options.debug){return}return console.log.apply(console,args)};return GIF}(EventEmitter);module.exports=GIF},{"./browser.coffee":2,events:1}]},{},[3])(3)}); +//# sourceMappingURL=gif.js.map diff --git a/static/js/gif.worker.js b/static/js/gif.worker.js new file mode 100644 index 0000000..269624e --- /dev/null +++ b/static/js/gif.worker.js @@ -0,0 +1,3 @@ +// gif.worker.js 0.2.0 - https://github.com/jnordberg/gif.js +(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o=ByteArray.pageSize)this.newPage();this.pages[this.page][this.cursor++]=val};ByteArray.prototype.writeUTFBytes=function(string){for(var l=string.length,i=0;i=0)this.dispose=disposalCode};GIFEncoder.prototype.setRepeat=function(repeat){this.repeat=repeat};GIFEncoder.prototype.setTransparent=function(color){this.transparent=color};GIFEncoder.prototype.addFrame=function(imageData){this.image=imageData;this.colorTab=this.globalPalette&&this.globalPalette.slice?this.globalPalette:null;this.getImagePixels();this.analyzePixels();if(this.globalPalette===true)this.globalPalette=this.colorTab;if(this.firstFrame){this.writeLSD();this.writePalette();if(this.repeat>=0){this.writeNetscapeExt()}}this.writeGraphicCtrlExt();this.writeImageDesc();if(!this.firstFrame&&!this.globalPalette)this.writePalette();this.writePixels();this.firstFrame=false};GIFEncoder.prototype.finish=function(){this.out.writeByte(59)};GIFEncoder.prototype.setQuality=function(quality){if(quality<1)quality=1;this.sample=quality};GIFEncoder.prototype.setDither=function(dither){if(dither===true)dither="FloydSteinberg";this.dither=dither};GIFEncoder.prototype.setGlobalPalette=function(palette){this.globalPalette=palette};GIFEncoder.prototype.getGlobalPalette=function(){return this.globalPalette&&this.globalPalette.slice&&this.globalPalette.slice(0)||this.globalPalette};GIFEncoder.prototype.writeHeader=function(){this.out.writeUTFBytes("GIF89a")};GIFEncoder.prototype.analyzePixels=function(){if(!this.colorTab){this.neuQuant=new NeuQuant(this.pixels,this.sample);this.neuQuant.buildColormap();this.colorTab=this.neuQuant.getColormap()}if(this.dither){this.ditherPixels(this.dither.replace("-serpentine",""),this.dither.match(/-serpentine/)!==null)}else{this.indexPixels()}this.pixels=null;this.colorDepth=8;this.palSize=7;if(this.transparent!==null){this.transIndex=this.findClosest(this.transparent,true)}};GIFEncoder.prototype.indexPixels=function(imgq){var nPix=this.pixels.length/3;this.indexedPixels=new Uint8Array(nPix);var k=0;for(var j=0;j=0&&x1+x=0&&y1+y>16,(c&65280)>>8,c&255,used)};GIFEncoder.prototype.findClosestRGB=function(r,g,b,used){if(this.colorTab===null)return-1;if(this.neuQuant&&!used){return this.neuQuant.lookupRGB(r,g,b)}var c=b|g<<8|r<<16;var minpos=0;var dmin=256*256*256;var len=this.colorTab.length;for(var i=0,index=0;i=0){disp=dispose&7}disp<<=2;this.out.writeByte(0|disp|0|transp);this.writeShort(this.delay);this.out.writeByte(this.transIndex);this.out.writeByte(0)};GIFEncoder.prototype.writeImageDesc=function(){this.out.writeByte(44);this.writeShort(0);this.writeShort(0);this.writeShort(this.width);this.writeShort(this.height);if(this.firstFrame||this.globalPalette){this.out.writeByte(0)}else{this.out.writeByte(128|0|0|0|this.palSize)}};GIFEncoder.prototype.writeLSD=function(){this.writeShort(this.width);this.writeShort(this.height);this.out.writeByte(128|112|0|this.palSize);this.out.writeByte(0);this.out.writeByte(0)};GIFEncoder.prototype.writeNetscapeExt=function(){this.out.writeByte(33);this.out.writeByte(255);this.out.writeByte(11);this.out.writeUTFBytes("NETSCAPE2.0");this.out.writeByte(3);this.out.writeByte(1);this.writeShort(this.repeat);this.out.writeByte(0)};GIFEncoder.prototype.writePalette=function(){this.out.writeBytes(this.colorTab);var n=3*256-this.colorTab.length;for(var i=0;i>8&255)};GIFEncoder.prototype.writePixels=function(){var enc=new LZWEncoder(this.width,this.height,this.indexedPixels,this.colorDepth);enc.encode(this.out)};GIFEncoder.prototype.stream=function(){return this.out};module.exports=GIFEncoder},{"./LZWEncoder.js":2,"./TypedNeuQuant.js":3}],2:[function(require,module,exports){var EOF=-1;var BITS=12;var HSIZE=5003;var masks=[0,1,3,7,15,31,63,127,255,511,1023,2047,4095,8191,16383,32767,65535];function LZWEncoder(width,height,pixels,colorDepth){var initCodeSize=Math.max(2,colorDepth);var accum=new Uint8Array(256);var htab=new Int32Array(HSIZE);var codetab=new Int32Array(HSIZE);var cur_accum,cur_bits=0;var a_count;var free_ent=0;var maxcode;var clear_flg=false;var g_init_bits,ClearCode,EOFCode;function char_out(c,outs){accum[a_count++]=c;if(a_count>=254)flush_char(outs)}function cl_block(outs){cl_hash(HSIZE);free_ent=ClearCode+2;clear_flg=true;output(ClearCode,outs)}function cl_hash(hsize){for(var i=0;i=0){disp=hsize_reg-i;if(i===0)disp=1;do{if((i-=disp)<0)i+=hsize_reg;if(htab[i]===fcode){ent=codetab[i];continue outer_loop}}while(htab[i]>=0)}output(ent,outs);ent=c;if(free_ent<1<0){outs.writeByte(a_count);outs.writeBytes(accum,0,a_count);a_count=0}}function MAXCODE(n_bits){return(1<0)cur_accum|=code<=8){char_out(cur_accum&255,outs);cur_accum>>=8;cur_bits-=8}if(free_ent>maxcode||clear_flg){if(clear_flg){maxcode=MAXCODE(n_bits=g_init_bits);clear_flg=false}else{++n_bits;if(n_bits==BITS)maxcode=1<0){char_out(cur_accum&255,outs);cur_accum>>=8;cur_bits-=8}flush_char(outs)}}this.encode=encode}module.exports=LZWEncoder},{}],3:[function(require,module,exports){var ncycles=100;var netsize=256;var maxnetpos=netsize-1;var netbiasshift=4;var intbiasshift=16;var intbias=1<>betashift;var betagamma=intbias<>3;var radiusbiasshift=6;var radiusbias=1<>3);var i,v;for(i=0;i>=netbiasshift;network[i][1]>>=netbiasshift;network[i][2]>>=netbiasshift;network[i][3]=i}}function altersingle(alpha,i,b,g,r){network[i][0]-=alpha*(network[i][0]-b)/initalpha;network[i][1]-=alpha*(network[i][1]-g)/initalpha;network[i][2]-=alpha*(network[i][2]-r)/initalpha}function alterneigh(radius,i,b,g,r){var lo=Math.abs(i-radius);var hi=Math.min(i+radius,netsize);var j=i+1;var k=i-1;var m=1;var p,a;while(jlo){a=radpower[m++];if(jlo){p=network[k--];p[0]-=a*(p[0]-b)/alpharadbias;p[1]-=a*(p[1]-g)/alpharadbias;p[2]-=a*(p[2]-r)/alpharadbias}}}function contest(b,g,r){var bestd=~(1<<31);var bestbiasd=bestd;var bestpos=-1;var bestbiaspos=bestpos;var i,n,dist,biasdist,betafreq;for(i=0;i>intbiasshift-netbiasshift);if(biasdist>betashift;freq[i]-=betafreq;bias[i]+=betafreq<>1;for(j=previouscol+1;j>1;for(j=previouscol+1;j<256;j++)netindex[j]=maxnetpos}function inxsearch(b,g,r){var a,p,dist;var bestd=1e3;var best=-1;var i=netindex[g];var j=i-1;while(i=0){if(i=bestd)i=netsize;else{i++;if(dist<0)dist=-dist;a=p[0]-b;if(a<0)a=-a;dist+=a;if(dist=0){p=network[j];dist=g-p[1];if(dist>=bestd)j=-1;else{j--;if(dist<0)dist=-dist;a=p[0]-b;if(a<0)a=-a;dist+=a;if(dist>radiusbiasshift;if(rad<=1)rad=0;for(i=0;i=lengthcount)pix-=lengthcount;i++;if(delta===0)delta=1;if(i%delta===0){alpha-=alpha/alphadec;radius-=radius/radiusdec;rad=radius>>radiusbiasshift;if(rad<=1)rad=0;for(j=0;j