mirror of
https://github.com/vee1e/itoa.git
synced 2026-09-01 11:08:20 +00:00
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:
parent
9dcd65a179
commit
b9acf8aad2
2 changed files with 387 additions and 82 deletions
282
src/App.css
282
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
187
src/App.jsx
187
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 = '<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, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||
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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
|
||||
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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
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 }}
|
||||
/>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue