From b9acf8aad27eb390d749f69700f1d8e535eae152 Mon Sep 17 00:00:00 2001 From: vee1e Date: Thu, 20 Nov 2025 02:37:51 +0530 Subject: [PATCH] feat: implement unique hover effects for Copy Text, Save SVG, and Save PNG buttons and apply general CSS formatting improvements. --- src/App.css | 282 ++++++++++++++++++++++++++++++++++++++++++---------- src/App.jsx | 187 ++++++++++++++++++++++++++++------ 2 files changed, 387 insertions(+), 82 deletions(-) diff --git a/src/App.css b/src/App.css index fba7c20..ec83a35 100644 --- a/src/App.css +++ b/src/App.css @@ -71,43 +71,55 @@ body { 0% { text-shadow: 0 0 5px var(--neon-pink); } + 3% { - text-shadow: + text-shadow: 0 0 10px var(--neon-pink), 0 0 20px var(--neon-pink); } + 4% { text-shadow: none; } + 6% { - text-shadow: + text-shadow: 0 0 10px var(--neon-pink), 0 0 20px var(--neon-pink); } + 7% { text-shadow: 0 0 5px var(--neon-pink); } + 8% { text-shadow: none; } + 9% { text-shadow: 0 0 5px var(--neon-pink); } + 10% { text-shadow: none; } + 11% { text-shadow: 0 0 5px var(--neon-pink); } + 50% { text-shadow: 0 0 5px var(--neon-pink); } + 51% { text-shadow: none; } + 52% { text-shadow: 0 0 5px var(--neon-pink); } + 100% { text-shadow: 0 0 5px var(--neon-pink); } @@ -117,33 +129,43 @@ body { 0% { clip-path: inset(20% -6px 70% 0); } + 10% { clip-path: inset(80% -6px 5% 0); } + 20% { clip-path: inset(20% -6px 85% 0); } + 30% { clip-path: inset(0% -6px 50% 0); } + 40% { clip-path: inset(60% -6px 30% 0); } + 50% { clip-path: inset(10% -6px 85% 0); } + 60% { clip-path: inset(40% -6px 43% 0); } + 70% { clip-path: inset(5% -6px 95% 0); } + 80% { clip-path: inset(70% -6px 20% 0); } + 90% { clip-path: inset(20% -6px 60% 0); } + 100% { clip-path: inset(60% -6px 40% 0); } @@ -153,33 +175,43 @@ body { 0% { clip-path: inset(40% 0 60% -6px); } + 10% { clip-path: inset(20% 0 80% -6px); } + 20% { clip-path: inset(60% 0 20% -6px); } + 30% { clip-path: inset(80% 0 5% -6px); } + 40% { clip-path: inset(10% 0 90% -6px); } + 50% { clip-path: inset(50% 0 30% -6px); } + 60% { clip-path: inset(30% 0 60% -6px); } + 70% { clip-path: inset(70% 0 20% -6px); } + 80% { clip-path: inset(15% 0 80% -6px); } + 90% { clip-path: inset(40% 0 50% -6px); } + 100% { clip-path: inset(80% 0 20% -6px); } @@ -281,12 +313,29 @@ body { } @keyframes glitch { - 0% { transform: translate(0) } - 20% { transform: translate(-2px, 2px) } - 40% { transform: translate(-2px, -2px) } - 60% { transform: translate(2px, 2px) } - 80% { transform: translate(2px, -2px) } - 100% { transform: translate(0) } + 0% { + transform: translate(0) + } + + 20% { + transform: translate(-2px, 2px) + } + + 40% { + transform: translate(-2px, -2px) + } + + 60% { + transform: translate(2px, 2px) + } + + 80% { + transform: translate(2px, -2px) + } + + 100% { + transform: translate(0) + } } .controls { @@ -337,59 +386,71 @@ body { } @keyframes text-flicker { - 0%, 100% { + + 0%, + 100% { opacity: 0; text-shadow: none; } + 15% { opacity: 1; - text-shadow: + text-shadow: -1px 0 var(--neon-blue), 1px 0 var(--neon-pink); } + 17% { opacity: 0; text-shadow: none; } + 20% { opacity: 1; - text-shadow: + text-shadow: 1px 0 var(--neon-blue), -1px 0 var(--neon-pink), 0 0 20px rgba(255, 43, 157, 0.5); } + 22% { opacity: 0; text-shadow: none; } + 35% { opacity: 1; - text-shadow: + text-shadow: -1px 0 var(--neon-pink), 1px 0 var(--neon-blue), 0 0 20px rgba(0, 243, 255, 0.5); } + 37% { opacity: 0; text-shadow: none; } + 40% { opacity: 1; - text-shadow: + text-shadow: 1px 0 var(--neon-pink), -1px 0 var(--neon-blue); } + 42% { opacity: 0; text-shadow: none; } + 85% { opacity: 1; - text-shadow: + text-shadow: -1px 0 var(--neon-blue), 1px 0 var(--neon-pink), 0 0 20px rgba(255, 43, 157, 0.5); } + 87% { opacity: 0; text-shadow: none; @@ -397,21 +458,19 @@ body { } .control-btn.active { - background-image: linear-gradient( - to right, - #ff0000 0%, - #ff0000 16.67%, - #ff9900 16.67%, - #ff9900 33.33%, - #33ff00 33.33%, - #33ff00 50%, - #00ffcc 50%, - #00ffcc 66.67%, - #0099ff 66.67%, - #0099ff 83.33%, - #ff00ff 83.33%, - #ff00ff 100% - ); + background-image: linear-gradient(to right, + #ff0000 0%, + #ff0000 16.67%, + #ff9900 16.67%, + #ff9900 33.33%, + #33ff00 33.33%, + #33ff00 50%, + #00ffcc 50%, + #00ffcc 66.67%, + #0099ff 66.67%, + #0099ff 83.33%, + #ff00ff 83.33%, + #ff00ff 100%); background-size: 24px 100%; animation: pixel-rainbow 2s linear infinite; color: var(--dark-bg); @@ -423,6 +482,7 @@ body { 0% { background-position: 0 0; } + 100% { background-position: 24px 0; } @@ -455,11 +515,9 @@ body { .control-btn[data-text="Upload"]:hover { border-color: #00ff00; color: #00ff00; - background: linear-gradient( - 180deg, - rgba(0, 255, 0, 0.1), - rgba(0, 255, 0, 0.05) - ); + background: linear-gradient(180deg, + rgba(0, 255, 0, 0.1), + rgba(0, 255, 0, 0.05)); text-shadow: 0 0 5px #00ff00; position: relative; overflow: hidden; @@ -472,12 +530,10 @@ body { left: 0; right: 0; bottom: 0; - background: linear-gradient( - 180deg, - transparent 0%, - rgba(0, 255, 0, 0.2) 50%, - transparent 100% - ); + background: linear-gradient(180deg, + transparent 0%, + rgba(0, 255, 0, 0.2) 50%, + transparent 100%); animation: matrix-rain 1s linear infinite; font-family: monospace; font-size: 10px; @@ -491,6 +547,7 @@ body { 0% { transform: translateY(-100%); } + 100% { transform: translateY(100%); } @@ -513,16 +570,14 @@ body { left: 0; right: 0; height: 3px; - background: linear-gradient( - 90deg, - transparent 0%, - #ff6600 20%, - #ff6600 80%, - transparent 100% - ); + background: linear-gradient(90deg, + transparent 0%, + #ff6600 20%, + #ff6600 80%, + transparent 100%); animation: crt-scan 1.5s linear infinite; opacity: 1; - box-shadow: + box-shadow: 0 0 10px #ff6600, 0 0 20px #ff6600; pointer-events: none; @@ -545,24 +600,31 @@ body { from { transform: translateY(-2px); } + to { - transform: translateY(38px); /* Slightly more than button height */ + transform: translateY(38px); + /* Slightly more than button height */ } } @keyframes crt-glitch { - 0%, 100% { + + 0%, + 100% { clip-path: inset(50% 0 50% 0); transform: translateX(0); } + 25% { clip-path: inset(20% 0 80% 0); transform: translateX(-1px); } + 50% { clip-path: inset(80% 0 20% 0); transform: translateX(1px); } + 75% { clip-path: inset(30% 0 70% 0); transform: translateX(-1px); @@ -583,10 +645,13 @@ body { /* Simple color shift animation for mobile */ @keyframes mobile-title-color { - 0%, 100% { + + 0%, + 100% { color: var(--neon-pink); text-shadow: 0 0 5px var(--neon-pink); } + 50% { color: var(--neon-blue); text-shadow: 0 0 5px var(--neon-blue); @@ -699,8 +764,10 @@ body { .drop-zone.zoomed { overflow: auto; cursor: move; - scrollbar-width: none; /* Firefox */ - -ms-overflow-style: none; /* IE and Edge */ + scrollbar-width: none; + /* Firefox */ + -ms-overflow-style: none; + /* IE and Edge */ } .drop-zone.zoomed::-webkit-scrollbar { @@ -838,6 +905,113 @@ body { box-shadow: 0 0 10px rgba(255, 43, 157, 0.3); } +/* Copy Text button hover effect */ +.control-btn[data-text="Copy Text"]:not(:disabled):hover { + border-color: #00ff00; + color: #00ff00; + text-shadow: 0 0 5px #00ff00; + background: rgba(0, 255, 0, 0.1); + position: relative; +} + +.control-btn[data-text="Copy Text"]:not(:disabled):hover::after { + content: "COPY TEXT"; + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + text-shadow: 1px 0 #00ff00, -1px 0 #00ff00; + animation: pulse-glow 1s infinite; +} + +@keyframes pulse-glow { + + 0%, + 100% { + text-shadow: 0 0 5px #00ff00; + } + + 50% { + text-shadow: 0 0 15px #00ff00, 0 0 25px #00ff00; + } +} + +/* Save SVG button hover effect */ +.control-btn[data-text="Save SVG"]:not(:disabled):hover { + border-color: #9d00ff; + color: #9d00ff; + text-shadow: 0 0 5px #9d00ff; + background: rgba(157, 0, 255, 0.1); + position: relative; +} + +.control-btn[data-text="Save SVG"]:not(:disabled):hover::before { + content: ""; + position: absolute; + inset: 0; + border: 2px solid #9d00ff; + animation: border-pulse 1.5s infinite; + pointer-events: none; +} + +@keyframes border-pulse { + + 0%, + 100% { + opacity: 1; + transform: scale(1); + } + + 50% { + opacity: 0.5; + transform: scale(1.05); + } +} + +/* Save PNG button hover effect (updated from Save) */ +.control-btn[data-text="Save PNG"]:not(:disabled):hover { + border-color: #ff6600; + color: #ff6600; + text-shadow: 0 0 5px #ff6600; + position: relative; + overflow: hidden; + background: rgba(255, 102, 0, 0.1); +} + +.control-btn[data-text="Save PNG"]:not(:disabled):hover::before { + content: ""; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 3px; + background: linear-gradient(90deg, + transparent 0%, + #ff6600 20%, + #ff6600 80%, + transparent 100%); + animation: crt-scan 1.5s linear infinite; + opacity: 1; + box-shadow: + 0 0 10px #ff6600, + 0 0 20px #ff6600; + pointer-events: none; +} + +.control-btn[data-text="Save PNG"]:not(:disabled):hover::after { + content: "SAVE PNG"; + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + text-shadow: 2px 0 #ff0000, -2px 0 #00ffff; + animation: crt-glitch 0.05s infinite; + background: none; + z-index: 2; +} + @media (max-width: 768px) { .char-controls { flex-direction: column; @@ -850,4 +1024,4 @@ body { width: 100%; max-width: none; } -} +} \ No newline at end of file diff --git a/src/App.jsx b/src/App.jsx index f8b264a..f1bc44d 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -98,11 +98,11 @@ function App() { const dropZone = document.querySelector('.drop-zone') const MAX_ASCII_SIZE = sizeMap[size] - + // Calculate dimensions while preserving exact aspect ratio const aspectRatio = img.width / img.height let charWidth, charHeight - + // Account for character aspect ratio (typically 0.6) const charAspectRatio = 0.6 const adjustedAspectRatio = aspectRatio / charAspectRatio @@ -139,11 +139,11 @@ function App() { document.documentElement.style.setProperty('--ascii-margin-y', `${marginY}px`) const canvas = document.createElement('canvas') - const ctx = canvas.getContext('2d') + const ctx = canvas.getContext('2d', { willReadFrequently: true }) canvas.width = charWidth canvas.height = charHeight ctx.drawImage(img, 0, 0, charWidth, charHeight) - + try { const imageData = ctx.getImageData(0, 0, charWidth, charHeight) const charsToUse = customChars || asciiChars @@ -163,14 +163,21 @@ function App() { const r = imageData.data[offset] const g = imageData.data[offset + 1] const b = imageData.data[offset + 2] - const brightness = 0.299 * r + 0.587 * g + 0.114 * b - const charIndex = Math.min(Math.floor((brightness / 255) * charArray.length), charArray.length - 1) - line.push(charArray[charIndex]) + const a = imageData.data[offset + 3] + + // Handle transparency - use space for fully transparent pixels + if (a < 128) { + line.push(' ') + } else { + const brightness = 0.299 * r + 0.587 * g + 0.114 * b + const charIndex = Math.min(Math.floor((brightness / 255) * charArray.length), charArray.length - 1) + line.push(charArray[charIndex]) + } } lines.push(line) monoAscii += line.join('') + '\n' } - + // Generate colored version using the exact same characters let colorHtml = '
'
       for (let y = 0; y < charHeight; y++) {
@@ -179,19 +186,23 @@ function App() {
           const r = imageData.data[offset]
           const g = imageData.data[offset + 1]
           const b = imageData.data[offset + 2]
+          const a = imageData.data[offset + 3]
           const char = lines[y][x] // Use the exact same character from monochrome version
           // Escape HTML special characters
           const escapedChar = char.replace(/&/g, '&').replace(//g, '>')
-          colorHtml += `${escapedChar}`
+
+          // Apply transparency to color
+          const alpha = (a / 255).toFixed(2)
+          colorHtml += `${escapedChar}`
         }
         colorHtml += '\n'
       }
       colorHtml += '
' - + // Store both versions setAsciiArt(monoAscii) setColorAsciiArt(colorHtml) - + } catch (error) { console.error('Error converting image to ASCII:', error) } finally { @@ -212,7 +223,7 @@ function App() { const handleDrop = useCallback((e) => { e.preventDefault() setIsDragging(false) - + const file = e.dataTransfer.files[0] if (file && file.type.startsWith('image/')) { convertToAscii(file) @@ -240,7 +251,7 @@ function App() { const canvas = document.createElement('canvas') const ctx = canvas.getContext('2d') - + // Get the number of characters in width and height const lines = asciiArt.split('\n').filter(line => line.length > 0) // Remove empty lines const charHeight = lines.length @@ -351,6 +362,109 @@ function App() { } } + const handleCopyText = async () => { + if (!asciiArt) return + + try { + await navigator.clipboard.writeText(asciiArt) + // Visual feedback - could add a toast notification here + console.log('ASCII art copied to clipboard!') + } catch (error) { + console.error('Failed to copy to clipboard:', error) + // Fallback for older browsers + const textArea = document.createElement('textarea') + textArea.value = asciiArt + textArea.style.position = 'fixed' + textArea.style.left = '-999999px' + document.body.appendChild(textArea) + textArea.select() + try { + document.execCommand('copy') + console.log('ASCII art copied to clipboard (fallback)!') + } catch (err) { + console.error('Fallback copy failed:', err) + } + document.body.removeChild(textArea) + } + } + + const handleSaveSVG = () => { + if (!asciiArt && !colorAsciiArt) return + + const lines = asciiArt.split('\n').filter(line => line.length > 0) + const charHeight = lines.length + const charWidth = lines[0].length + + // SVG character dimensions + const charWidthPx = 10 + const charHeightPx = 16 + const svgWidth = charWidth * charWidthPx + const svgHeight = charHeight * charHeightPx + + let svgContent = `\n` + svgContent += `\n` + svgContent += ` \n` + svgContent += ` \n` + + if (useColor) { + // Parse colored HTML to extract colors + const tempDiv = document.createElement('div') + tempDiv.innerHTML = colorAsciiArt + const pre = tempDiv.querySelector('pre') + const spans = Array.from(pre.querySelectorAll('span')) + let spanIndex = 0 + + lines.forEach((line, lineIndex) => { + for (let charIndex = 0; charIndex < line.length; charIndex++) { + if (spanIndex < spans.length) { + const span = spans[spanIndex] + const color = span.style.color + const char = span.textContent + + // Escape XML special characters + const escapedChar = char + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') + + const x = charIndex * charWidthPx + const y = (lineIndex + 1) * charHeightPx - 3 + + svgContent += ` ${escapedChar}\n` + spanIndex++ + } + } + }) + } else { + // Monochrome version + lines.forEach((line, lineIndex) => { + const y = (lineIndex + 1) * charHeightPx - 3 + const escapedLine = line + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') + svgContent += ` ${escapedLine}\n` + }) + } + + svgContent += `` + + // Create and download the SVG file + const blob = new Blob([svgContent], { type: 'image/svg+xml' }) + const url = URL.createObjectURL(blob) + const link = document.createElement('a') + link.download = 'ascii-art.svg' + link.href = url + link.click() + setTimeout(() => URL.revokeObjectURL(url), 100) + } + // Add favicon animation useEffect(() => { const colors = ['#00f3ff', '#ff2b9d'] @@ -367,6 +481,13 @@ function App() { return () => clearInterval(faviconInterval) }, []) + // Instant regeneration when settings change + useEffect(() => { + if (lastImageFile) { + convertToAscii(lastImageFile, asciiChars) + } + }, [size, useColor, asciiChars, convertToAscii]) + return (

ITOA: Image to ASCII Converter

@@ -391,24 +512,16 @@ function App() { onChange={(e) => setAsciiChars(e.target.value)} placeholder="ASCII characters" /> -
- - + + + + + {(asciiArt || colorAsciiArt) && ( @@ -452,7 +583,7 @@ function App() { )}
- +
1 ? 'zoomed' : ''}`} onDragOver={handleDragOver} @@ -462,7 +593,7 @@ function App() { > {(asciiArt || colorAsciiArt) ? ( useColor ? ( -