feat: implement unique hover effects for Copy Text, Save SVG, and Save PNG buttons and apply general CSS formatting improvements.

This commit is contained in:
vee1e 2025-11-20 02:37:51 +05:30
parent 9dcd65a179
commit b9acf8aad2
2 changed files with 387 additions and 82 deletions

View file

@ -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;
}
}
}

View file

@ -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 = '<pre style="margin:0; padding:0; white-space:pre;">'
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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
colorHtml += `<span style="color:rgb(${r},${g},${b})">${escapedChar}</span>`
// Apply transparency to color
const alpha = (a / 255).toFixed(2)
colorHtml += `<span style="color:rgba(${r},${g},${b},${alpha})">${escapedChar}</span>`
}
colorHtml += '\n'
}
colorHtml += '</pre>'
// 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 = `<?xml version="1.0" encoding="UTF-8"?>\n`
svgContent += `<svg xmlns="http://www.w3.org/2000/svg" width="${svgWidth}" height="${svgHeight}" viewBox="0 0 ${svgWidth} ${svgHeight}">\n`
svgContent += ` <rect width="${svgWidth}" height="${svgHeight}" fill="#0a0a0f"/>\n`
svgContent += ` <style>\n`
svgContent += ` text { font-family: 'Courier New', monospace; font-size: 14px; font-weight: bold; }\n`
svgContent += ` </style>\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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&apos;')
const x = charIndex * charWidthPx
const y = (lineIndex + 1) * charHeightPx - 3
svgContent += ` <text x="${x}" y="${y}" fill="${color}">${escapedChar}</text>\n`
spanIndex++
}
}
})
} else {
// Monochrome version
lines.forEach((line, lineIndex) => {
const y = (lineIndex + 1) * charHeightPx - 3
const escapedLine = line
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&apos;')
svgContent += ` <text x="0" y="${y}" fill="#ff2b9d">${escapedLine}</text>\n`
})
}
svgContent += `</svg>`
// 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 (
<div className="app-container">
<h1 className="title">ITOA: Image to ASCII Converter</h1>
@ -391,24 +512,16 @@ function App() {
onChange={(e) => setAsciiChars(e.target.value)}
placeholder="ASCII characters"
/>
<button
className="control-btn"
onClick={handleRegenerate}
disabled={!lastImageFile}
data-text="Regenerate"
>
Regenerate
</button>
</div>
<div className="controls">
<button
<button
className={`control-btn ${useColor ? 'active' : ''}`}
onClick={() => setUseColor(prev => !prev)}
>
{useColor ? 'Colors' : 'Monochrome'}
</button>
<button
className="control-btn"
onClick={cycleSize}
@ -424,7 +537,7 @@ function App() {
accept="image/*"
style={{ display: 'none' }}
/>
<button
className="control-btn"
onClick={() => fileInputRef.current.click()}
@ -437,9 +550,27 @@ function App() {
className="control-btn"
onClick={handleSaveAscii}
disabled={!asciiArt && !colorAsciiArt}
data-text="Save"
data-text="Save PNG"
>
Save
Save PNG
</button>
<button
className="control-btn"
onClick={handleSaveSVG}
disabled={!asciiArt && !colorAsciiArt}
data-text="Save SVG"
>
Save SVG
</button>
<button
className="control-btn"
onClick={handleCopyText}
disabled={!asciiArt}
data-text="Copy Text"
>
Copy Text
</button>
{(asciiArt || colorAsciiArt) && (
@ -452,7 +583,7 @@ function App() {
</button>
)}
</div>
<div
className={`drop-zone ${isDragging ? 'dragging' : ''} ${zoomLevel > 1 ? 'zoomed' : ''}`}
onDragOver={handleDragOver}
@ -462,7 +593,7 @@ function App() {
>
{(asciiArt || colorAsciiArt) ? (
useColor ? (
<pre
<pre
className="ascii-output"
dangerouslySetInnerHTML={{ __html: colorAsciiArt }}
/>