Initial commit: Text Corruptor PWA

Features:
- Zalgo text generator with adjustable intensity (1-10)
- Real-time text corruption as you type
- Click-to-copy functionality with visual feedback
- Progressive Web App with offline support
- Responsive design for mobile and desktop
- Dark theme with glitch-inspired aesthetics

Technical implementation:
- Pure JavaScript implementation (no frameworks)
- Service Worker for offline functionality
- PWA manifest for installability
- Python development server
- Comprehensive linting setup (ESLint, Prettier, Black, Pylint)

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Ole-Morten Duesund 2025-08-18 20:00:58 +02:00
commit 44a2ac4cbd
23 changed files with 1672 additions and 0 deletions

122
app/app.js Normal file
View file

@ -0,0 +1,122 @@
/**
* Main application logic for Text Corruptor
* Handles UI interactions and PWA registration
*/
/* global ZalgoGenerator */
// Initialize zalgo generator
const zalgo = new ZalgoGenerator();
// DOM elements
const inputText = document.getElementById('inputText');
const outputText = document.getElementById('outputText');
const intensitySlider = document.getElementById('intensity');
const intensityValue = document.getElementById('intensityValue');
const clearBtn = document.getElementById('clearBtn');
const copyNotification = document.getElementById('copyNotification');
/**
* Update the corrupted text output
*/
function updateOutput() {
const text = inputText.value;
const intensity = parseInt(intensitySlider.value);
if (text) {
outputText.value = zalgo.generate(text, intensity);
} else {
outputText.value = '';
}
}
/**
* Copy text to clipboard and show notification
*/
async function copyToClipboard() {
if (!outputText.value) return;
try {
await navigator.clipboard.writeText(outputText.value);
// Show copy notification
copyNotification.classList.add('show');
setTimeout(() => {
copyNotification.classList.remove('show');
}, 2000);
} catch (err) {
// Fallback for older browsers
outputText.select();
document.execCommand('copy');
// Show copy notification
copyNotification.classList.add('show');
setTimeout(() => {
copyNotification.classList.remove('show');
}, 2000);
}
}
/**
* Clear all text fields
*/
function clearAll() {
inputText.value = '';
outputText.value = '';
inputText.focus();
}
// Event listeners
inputText.addEventListener('input', updateOutput);
intensitySlider.addEventListener('input', () => {
intensityValue.textContent = intensitySlider.value;
updateOutput();
});
outputText.addEventListener('click', copyToClipboard);
clearBtn.addEventListener('click', clearAll);
// Handle keyboard shortcuts
document.addEventListener('keydown', e => {
// Ctrl/Cmd + K to clear
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
e.preventDefault();
clearAll();
}
// Ctrl/Cmd + C when output is focused to copy
if ((e.ctrlKey || e.metaKey) && e.key === 'c' && document.activeElement === outputText) {
copyToClipboard();
}
});
// Register service worker for PWA functionality
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker
.register('sw.js')
.then(registration => {
console.log('Service Worker registered successfully:', registration.scope);
})
.catch(error => {
console.log('Service Worker registration failed:', error);
});
});
}
// Handle install prompt for PWA
window.addEventListener('beforeinstallprompt', e => {
// Prevent the mini-infobar from appearing on mobile
e.preventDefault();
// Store the event so it can be triggered later if needed
// Currently not used but kept for potential future install button
window.deferredPrompt = e;
console.log('Install prompt ready');
});
// Focus input on load
window.addEventListener('load', () => {
inputText.focus();
});

96
app/create-icons.js Normal file
View file

@ -0,0 +1,96 @@
/**
* Node.js script to generate PWA icons programmatically
* Run with: node create-icons.js
* Requires: npm install canvas
*/
/* eslint-env node */
/* eslint-disable no-undef */
const fs = require('fs');
const path = require('path');
// Check if we're in a browser or Node.js environment
const isBrowser = typeof window !== 'undefined';
if (!isBrowser) {
// Node.js environment - use canvas package
try {
const { createCanvas } = require('canvas');
// Icon sizes needed for PWA
const sizes = [72, 96, 128, 144, 152, 192, 384, 512];
// Create icons directory if it doesn't exist
const iconsDir = path.join(__dirname, 'icons');
if (!fs.existsSync(iconsDir)) {
fs.mkdirSync(iconsDir, { recursive: true });
}
const createIcon = function (size) {
const canvas = createCanvas(size, size);
const ctx = canvas.getContext('2d');
// Background gradient (solid color fallback for canvas)
ctx.fillStyle = '#1a1a2e';
ctx.fillRect(0, 0, size, size);
// Center circle
const centerX = size / 2;
const centerY = size / 2;
const radius = size * 0.35;
ctx.beginPath();
ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
ctx.fillStyle = '#00ff88';
ctx.fill();
// Letter "T"
ctx.font = `bold ${size * 0.4}px Arial`;
ctx.fillStyle = '#0f0f14';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('T', centerX, centerY);
return canvas;
};
// Generate all icons
sizes.forEach(size => {
const canvas = createIcon(size);
const buffer = canvas.toBuffer('image/png');
const filename = path.join(iconsDir, `icon-${size}.png`);
fs.writeFileSync(filename, buffer);
console.log(`Created ${filename}`);
});
console.log('All icons created successfully!');
} catch (error) {
console.log('Canvas package not installed. Creating placeholder icons instead...');
// Fallback: Create placeholder SVG icons
const sizes = [72, 96, 128, 144, 152, 192, 384, 512];
const iconsDir = path.join(__dirname, 'icons');
if (!fs.existsSync(iconsDir)) {
fs.mkdirSync(iconsDir, { recursive: true });
}
sizes.forEach(size => {
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 ${size} ${size}">
<rect width="${size}" height="${size}" fill="#1a1a2e"/>
<circle cx="${size / 2}" cy="${size / 2}" r="${size * 0.35}" fill="#00ff88"/>
<text x="${size / 2}" y="${size / 2}" font-family="Arial" font-size="${size * 0.4}" font-weight="bold" text-anchor="middle" dominant-baseline="middle" fill="#0f0f14">T</text>
</svg>`;
const filename = path.join(iconsDir, `icon-${size}.svg`);
fs.writeFileSync(filename, svg);
console.log(`Created ${filename} (SVG placeholder)`);
});
console.log('\nNote: SVG placeholders created. For PNG icons, install canvas package:');
console.log('npm install canvas');
}
} else {
console.log('This script should be run in Node.js, not in a browser.');
}

156
app/generate-icons.html Normal file
View file

@ -0,0 +1,156 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Generate PWA Icons</title>
</head>
<body>
<h1>Icon Generator for Text Corruptor</h1>
<p>Right-click each canvas and save as PNG to the icons folder</p>
<div id="canvases"></div>
<script>
// Icon sizes needed for PWA
const sizes = [72, 96, 128, 144, 152, 192, 384, 512];
function createIcon(size) {
const canvas = document.createElement('canvas');
canvas.width = size;
canvas.height = size;
const ctx = canvas.getContext('2d');
// Background gradient
const gradient = ctx.createLinearGradient(0, 0, size, size);
gradient.addColorStop(0, '#1a1a2e');
gradient.addColorStop(1, '#0f0f14');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, size, size);
// Add glitch lines effect
ctx.strokeStyle = 'rgba(0, 255, 136, 0.1)';
ctx.lineWidth = 1;
for (let i = 0; i < size; i += 4) {
ctx.beginPath();
ctx.moveTo(0, i);
ctx.lineTo(size, i);
ctx.stroke();
}
// Center circle background
const centerX = size / 2;
const centerY = size / 2;
const radius = size * 0.35;
ctx.beginPath();
ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
const circleGradient = ctx.createRadialGradient(
centerX,
centerY,
0,
centerX,
centerY,
radius
);
circleGradient.addColorStop(0, '#00ff88');
circleGradient.addColorStop(1, '#00cc6a');
ctx.fillStyle = circleGradient;
ctx.fill();
// Add corrupted "T" letter with zalgo effect simulation
ctx.font = `bold ${size * 0.4}px Arial`;
ctx.fillStyle = '#0f0f14';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
// Main letter
ctx.fillText('T', centerX, centerY);
// Simulated zalgo corruption marks
ctx.fillStyle = 'rgba(15, 15, 20, 0.5)';
const corruption = ['̴', '̸', '̶', '̷', '̵'];
const offsetX = size * 0.02;
const offsetY = size * 0.02;
// Add small corruption marks around the letter
for (let i = 0; i < 3; i++) {
const x = centerX + (Math.random() - 0.5) * offsetX * 2;
const y = centerY + (Math.random() - 0.5) * offsetY * 2;
ctx.font = `${size * 0.1}px Arial`;
ctx.fillText('░', x, y - size * 0.15);
ctx.fillText('▒', x + offsetX, y + size * 0.15);
}
// Add glitch effect
ctx.strokeStyle = '#ff3366';
ctx.lineWidth = 1;
ctx.globalAlpha = 0.3;
ctx.strokeText('T', centerX - 2, centerY);
ctx.strokeStyle = '#00ffff';
ctx.strokeText('T', centerX + 2, centerY);
ctx.globalAlpha = 1;
// Add label
const label = document.createElement('div');
label.textContent = `icon-${size}.png`;
label.style.marginTop = '10px';
label.style.marginBottom = '20px';
const container = document.createElement('div');
container.appendChild(canvas);
container.appendChild(label);
return container;
}
// Generate all icons
const container = document.getElementById('canvases');
sizes.forEach(size => {
container.appendChild(createIcon(size));
});
// Auto-download function (optional - uncomment to use)
/*
function downloadCanvas(canvas, filename) {
canvas.toBlob(blob => {
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
});
}
// Auto-download all icons
setTimeout(() => {
const canvases = document.querySelectorAll('canvas');
canvases.forEach((canvas, index) => {
setTimeout(() => {
downloadCanvas(canvas, `icon-${sizes[index]}.png`);
}, index * 500);
});
}, 1000);
*/
</script>
<style>
body {
font-family: Arial, sans-serif;
padding: 20px;
background: #f0f0f0;
}
canvas {
border: 1px solid #ccc;
display: block;
background: white;
}
#canvases > div {
display: inline-block;
margin: 10px;
text-align: center;
}
</style>
</body>
</html>

5
app/icons/icon-128.svg Normal file
View file

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 128 128">
<rect width="128" height="128" fill="#1a1a2e"/>
<circle cx="64" cy="64" r="44.8" fill="#00ff88"/>
<text x="64" y="64" font-family="Arial" font-size="51.2" font-weight="bold" text-anchor="middle" dominant-baseline="middle" fill="#0f0f14">T</text>
</svg>

After

Width:  |  Height:  |  Size: 400 B

5
app/icons/icon-144.svg Normal file
View file

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="144" height="144" viewBox="0 0 144 144">
<rect width="144" height="144" fill="#1a1a2e"/>
<circle cx="72" cy="72" r="50.4" fill="#00ff88"/>
<text x="72" y="72" font-family="Arial" font-size="57.6" font-weight="bold" text-anchor="middle" dominant-baseline="middle" fill="#0f0f14">T</text>
</svg>

After

Width:  |  Height:  |  Size: 400 B

5
app/icons/icon-152.svg Normal file
View file

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="152" height="152" viewBox="0 0 152 152">
<rect width="152" height="152" fill="#1a1a2e"/>
<circle cx="76" cy="76" r="53.199999999999996" fill="#00ff88"/>
<text x="76" y="76" font-family="Arial" font-size="60.800000000000004" font-weight="bold" text-anchor="middle" dominant-baseline="middle" fill="#0f0f14">T</text>
</svg>

After

Width:  |  Height:  |  Size: 428 B

5
app/icons/icon-192.svg Normal file
View file

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="192" height="192" viewBox="0 0 192 192">
<rect width="192" height="192" fill="#1a1a2e"/>
<circle cx="96" cy="96" r="67.19999999999999" fill="#00ff88"/>
<text x="96" y="96" font-family="Arial" font-size="76.80000000000001" font-weight="bold" text-anchor="middle" dominant-baseline="middle" fill="#0f0f14">T</text>
</svg>

After

Width:  |  Height:  |  Size: 426 B

5
app/icons/icon-384.svg Normal file
View file

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="384" height="384" viewBox="0 0 384 384">
<rect width="384" height="384" fill="#1a1a2e"/>
<circle cx="192" cy="192" r="134.39999999999998" fill="#00ff88"/>
<text x="192" y="192" font-family="Arial" font-size="153.60000000000002" font-weight="bold" text-anchor="middle" dominant-baseline="middle" fill="#0f0f14">T</text>
</svg>

After

Width:  |  Height:  |  Size: 432 B

5
app/icons/icon-512.svg Normal file
View file

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512">
<rect width="512" height="512" fill="#1a1a2e"/>
<circle cx="256" cy="256" r="179.2" fill="#00ff88"/>
<text x="256" y="256" font-family="Arial" font-size="204.8" font-weight="bold" text-anchor="middle" dominant-baseline="middle" fill="#0f0f14">T</text>
</svg>

After

Width:  |  Height:  |  Size: 406 B

5
app/icons/icon-72.svg Normal file
View file

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="72" height="72" viewBox="0 0 72 72">
<rect width="72" height="72" fill="#1a1a2e"/>
<circle cx="36" cy="36" r="25.2" fill="#00ff88"/>
<text x="36" y="36" font-family="Arial" font-size="28.8" font-weight="bold" text-anchor="middle" dominant-baseline="middle" fill="#0f0f14">T</text>
</svg>

After

Width:  |  Height:  |  Size: 394 B

5
app/icons/icon-96.svg Normal file
View file

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="96" height="96" viewBox="0 0 96 96">
<rect width="96" height="96" fill="#1a1a2e"/>
<circle cx="48" cy="48" r="33.599999999999994" fill="#00ff88"/>
<text x="48" y="48" font-family="Arial" font-size="38.400000000000006" font-weight="bold" text-anchor="middle" dominant-baseline="middle" fill="#0f0f14">T</text>
</svg>

After

Width:  |  Height:  |  Size: 422 B

66
app/index.html Normal file
View file

@ -0,0 +1,66 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta
name="description"
content="Transform your text into corrupted zalgo text with one click. Perfect for creating glitchy, distorted text effects."
/>
<meta name="theme-color" content="#1a1a2e" />
<title>Text Corruptor - Zalgo Text Generator</title>
<link rel="manifest" href="manifest.json" />
<link rel="icon" type="image/png" sizes="192x192" href="icons/icon-192.png" />
<link rel="icon" type="image/png" sizes="512x512" href="icons/icon-512.png" />
<link rel="apple-touch-icon" href="icons/icon-192.png" />
<link rel="stylesheet" href="styles.css" />
</head>
<body>
<div class="container">
<header>
<h1>Text Corruptor</h1>
<p class="subtitle">Transform your text into corrupted chaos</p>
</header>
<main>
<div class="controls">
<label for="intensity">Corruption Intensity:</label>
<input type="range" id="intensity" min="1" max="10" value="5" />
<span id="intensityValue">5</span>
</div>
<div class="text-area-container">
<label for="inputText">Original Text</label>
<textarea
id="inputText"
placeholder="Enter your text here..."
rows="6"
autocomplete="off"
spellcheck="false"
></textarea>
</div>
<div class="text-area-container">
<label for="outputText">Corrupted Text</label>
<textarea
id="outputText"
placeholder="Your corrupted text will appear here..."
rows="6"
readonly
title="Click to copy"
></textarea>
<div id="copyNotification" class="copy-notification">Copied!</div>
</div>
<button id="clearBtn" class="clear-btn">Clear All</button>
</main>
<footer>
<p>Click the corrupted text to copy it to your clipboard</p>
</footer>
</div>
<script src="zalgo.js"></script>
<script src="app.js"></script>
</body>
</html>

62
app/manifest.json Normal file
View file

@ -0,0 +1,62 @@
{
"name": "Text Corruptor - Zalgo Text Generator",
"short_name": "Text Corruptor",
"description": "Transform your text into corrupted zalgo text with adjustable intensity",
"start_url": "/",
"display": "standalone",
"background_color": "#0f0f14",
"theme_color": "#1a1a2e",
"orientation": "portrait-primary",
"icons": [
{
"src": "icons/icon-72.png",
"sizes": "72x72",
"type": "image/png"
},
{
"src": "icons/icon-96.png",
"sizes": "96x96",
"type": "image/png"
},
{
"src": "icons/icon-128.png",
"sizes": "128x128",
"type": "image/png"
},
{
"src": "icons/icon-144.png",
"sizes": "144x144",
"type": "image/png"
},
{
"src": "icons/icon-152.png",
"sizes": "152x152",
"type": "image/png"
},
{
"src": "icons/icon-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "icons/icon-384.png",
"sizes": "384x384",
"type": "image/png"
},
{
"src": "icons/icon-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any maskable"
}
],
"categories": ["utilities", "productivity"],
"screenshots": [
{
"src": "screenshots/screenshot1.png",
"type": "image/png",
"sizes": "1280x720"
}
]
}

344
app/styles.css Normal file
View file

@ -0,0 +1,344 @@
/**
* Styles for Text Corruptor
* Dark theme with glitch-inspired aesthetics
*/
:root {
--bg-primary: #0f0f14;
--bg-secondary: #1a1a2e;
--bg-tertiary: #16213e;
--text-primary: #eaeaea;
--text-secondary: #a8a8b3;
--accent: #00ff88;
--accent-hover: #00cc6a;
--error: #ff3366;
--border: #2a2a3e;
--shadow: rgba(0, 255, 136, 0.1);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
background-image: repeating-linear-gradient(
0deg,
transparent,
transparent 2px,
rgba(0, 255, 136, 0.03) 2px,
rgba(0, 255, 136, 0.03) 4px
);
}
.container {
width: 100%;
max-width: 800px;
background: var(--bg-secondary);
border-radius: 16px;
padding: 40px;
box-shadow:
0 10px 40px rgba(0, 0, 0, 0.5),
0 0 100px var(--shadow);
border: 1px solid var(--border);
}
header {
text-align: center;
margin-bottom: 40px;
}
h1 {
font-size: 2.5rem;
font-weight: 700;
margin-bottom: 10px;
background: linear-gradient(135deg, var(--accent), #00ffff);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
text-shadow: 0 0 40px var(--shadow);
animation: glitch 3s infinite;
}
@keyframes glitch {
0%,
100% {
text-shadow: 0 0 40px var(--shadow);
}
25% {
text-shadow:
-2px 0 var(--error),
2px 0 var(--accent);
}
50% {
text-shadow:
2px 0 var(--error),
-2px 0 var(--accent);
}
75% {
text-shadow: 0 0 40px var(--shadow);
}
}
.subtitle {
color: var(--text-secondary);
font-size: 1.1rem;
}
.controls {
display: flex;
align-items: center;
gap: 15px;
margin-bottom: 30px;
padding: 20px;
background: var(--bg-tertiary);
border-radius: 12px;
border: 1px solid var(--border);
}
.controls label {
color: var(--text-secondary);
font-size: 0.95rem;
}
#intensity {
flex: 1;
-webkit-appearance: none;
appearance: none;
height: 6px;
background: var(--border);
border-radius: 3px;
outline: none;
transition: all 0.3s ease;
}
#intensity::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 20px;
height: 20px;
background: var(--accent);
border-radius: 50%;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 0 10px var(--shadow);
}
#intensity::-moz-range-thumb {
width: 20px;
height: 20px;
background: var(--accent);
border-radius: 50%;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 0 10px var(--shadow);
}
#intensity:hover::-webkit-slider-thumb {
transform: scale(1.2);
box-shadow: 0 0 20px var(--shadow);
}
#intensity:hover::-moz-range-thumb {
transform: scale(1.2);
box-shadow: 0 0 20px var(--shadow);
}
#intensityValue {
min-width: 30px;
text-align: center;
font-weight: 600;
color: var(--accent);
font-size: 1.1rem;
}
.text-area-container {
margin-bottom: 25px;
position: relative;
}
.text-area-container label {
display: block;
margin-bottom: 10px;
color: var(--text-secondary);
font-size: 0.95rem;
text-transform: uppercase;
letter-spacing: 1px;
}
textarea {
width: 100%;
min-height: 150px;
padding: 15px;
background: var(--bg-tertiary);
border: 2px solid var(--border);
border-radius: 12px;
color: var(--text-primary);
font-size: 1rem;
font-family: 'Courier New', monospace;
resize: vertical;
outline: none;
transition: all 0.3s ease;
}
textarea:focus {
border-color: var(--accent);
box-shadow: 0 0 20px var(--shadow);
}
#outputText {
cursor: pointer;
position: relative;
}
#outputText:hover {
background: rgba(0, 255, 136, 0.05);
}
.copy-notification {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: var(--accent);
color: var(--bg-primary);
padding: 12px 24px;
border-radius: 8px;
font-weight: 600;
opacity: 0;
pointer-events: none;
transition: all 0.3s ease;
z-index: 10;
}
.copy-notification.show {
opacity: 1;
animation: pulse 0.5s ease;
}
@keyframes pulse {
0% {
transform: translate(-50%, -50%) scale(0.8);
}
50% {
transform: translate(-50%, -50%) scale(1.1);
}
100% {
transform: translate(-50%, -50%) scale(1);
}
}
.clear-btn {
width: 100%;
padding: 15px;
background: transparent;
border: 2px solid var(--accent);
border-radius: 12px;
color: var(--accent);
font-size: 1rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 1px;
cursor: pointer;
transition: all 0.3s ease;
margin-top: 20px;
}
.clear-btn:hover {
background: var(--accent);
color: var(--bg-primary);
box-shadow: 0 0 30px var(--shadow);
transform: translateY(-2px);
}
.clear-btn:active {
transform: translateY(0);
}
footer {
text-align: center;
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid var(--border);
}
footer p {
color: var(--text-secondary);
font-size: 0.9rem;
}
/* Mobile responsiveness */
@media (max-width: 600px) {
.container {
padding: 25px;
}
h1 {
font-size: 2rem;
}
.controls {
flex-direction: column;
align-items: stretch;
gap: 10px;
}
textarea {
min-height: 120px;
font-size: 0.95rem;
}
}
/* Glitch effect for corrupted text */
#outputText {
animation: subtle-glitch 5s infinite;
}
@keyframes subtle-glitch {
0%,
100% {
filter: none;
}
92% {
filter: none;
}
93% {
filter: drop-shadow(-2px 0 var(--error)) drop-shadow(2px 0 var(--accent));
}
94% {
filter: none;
}
}
/* Loading state */
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* Print styles */
@media print {
body {
background: white;
color: black;
}
.container {
box-shadow: none;
border: 1px solid #ccc;
}
.clear-btn {
display: none;
}
}

90
app/sw.js Normal file
View file

@ -0,0 +1,90 @@
/**
* Service Worker for Text Corruptor PWA
* Enables offline functionality and caching
*/
const CACHE_NAME = 'text-corruptor-v1';
const urlsToCache = ['/', '/index.html', '/styles.css', '/zalgo.js', '/app.js', '/manifest.json'];
// Install event - cache essential files
self.addEventListener('install', event => {
event.waitUntil(
caches
.open(CACHE_NAME)
.then(cache => {
console.log('Opened cache');
return cache.addAll(urlsToCache);
})
.then(() => {
// Force the service worker to become active immediately
return self.skipWaiting();
})
);
});
// Fetch event - serve from cache when offline
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request).then(response => {
// Cache hit - return response
if (response) {
return response;
}
// Clone the request because it's a stream
const fetchRequest = event.request.clone();
return fetch(fetchRequest)
.then(response => {
// Check if we received a valid response
if (!response || response.status !== 200 || response.type !== 'basic') {
return response;
}
// Clone the response because it's a stream
const responseToCache = response.clone();
caches.open(CACHE_NAME).then(cache => {
cache.put(event.request, responseToCache);
});
return response;
})
.catch(() => {
// Network request failed, serve offline fallback if available
return caches.match('/index.html');
});
})
);
});
// Activate event - clean up old caches
self.addEventListener('activate', event => {
const cacheWhitelist = [CACHE_NAME];
event.waitUntil(
caches
.keys()
.then(cacheNames => {
return Promise.all(
cacheNames.map(cacheName => {
if (cacheWhitelist.indexOf(cacheName) === -1) {
console.log('Deleting old cache:', cacheName);
return caches.delete(cacheName);
}
})
);
})
.then(() => {
// Ensure the service worker takes control immediately
return self.clients.claim();
})
);
});
// Handle messages from the client
self.addEventListener('message', event => {
if (event.data && event.data.type === 'SKIP_WAITING') {
self.skipWaiting();
}
});

267
app/zalgo.js Normal file
View file

@ -0,0 +1,267 @@
/**
* Zalgo Text Generator
* Generates corrupted/glitched text using Unicode combining characters
*/
/* exported ZalgoGenerator */
class ZalgoGenerator {
constructor() {
// Combining diacritical marks that appear above characters
this.above = [
'\u0300',
'\u0301',
'\u0302',
'\u0303',
'\u0304',
'\u0305',
'\u0306',
'\u0307',
'\u0308',
'\u0309',
'\u030A',
'\u030B',
'\u030C',
'\u030D',
'\u030E',
'\u030F',
'\u0310',
'\u0311',
'\u0312',
'\u0313',
'\u0314',
'\u0315',
'\u0316',
'\u0317',
'\u0318',
'\u0319',
'\u031A',
'\u031B',
'\u031C',
'\u031D',
'\u031E',
'\u031F',
'\u0320',
'\u0321',
'\u0322',
'\u0323',
'\u0324',
'\u0325',
'\u0326',
'\u0327',
'\u0328',
'\u0329',
'\u032A',
'\u032B',
'\u032C',
'\u032D',
'\u032E',
'\u032F',
'\u0330',
'\u0331',
'\u0332',
'\u0333',
'\u0334',
'\u0335',
'\u0336',
'\u0337',
'\u0338',
'\u0339',
'\u033A',
'\u033B',
'\u033C',
'\u033D',
'\u033E',
'\u033F',
'\u0340',
'\u0341',
'\u0342',
'\u0343',
'\u0344',
'\u0345',
'\u0346',
'\u0347',
'\u0348',
'\u0349',
'\u034A',
'\u034B',
'\u034C',
'\u034D',
'\u034E',
'\u034F',
'\u0350',
'\u0351',
'\u0352',
'\u0353',
'\u0354',
'\u0355',
'\u0356',
'\u0357',
'\u0358',
'\u0359',
'\u035A',
'\u035B',
'\u035C',
'\u035D',
'\u035E',
'\u035F',
'\u0360',
'\u0361',
'\u0362',
'\u0363',
'\u0364',
'\u0365',
'\u0366',
'\u0367',
'\u0368',
'\u0369',
'\u036A',
'\u036B',
'\u036C',
'\u036D',
'\u036E',
'\u036F',
];
// Combining diacritical marks that appear in the middle
this.middle = [
'\u0315',
'\u031B',
'\u0340',
'\u0341',
'\u0358',
'\u0321',
'\u0322',
'\u0327',
'\u0328',
'\u0334',
'\u0335',
'\u0336',
'\u034F',
'\u035C',
'\u035D',
'\u035E',
'\u035F',
'\u0360',
'\u0362',
'\u0338',
'\u0337',
'\u0361',
'\u0489',
];
// Combining diacritical marks that appear below characters
this.below = [
'\u0316',
'\u0317',
'\u0318',
'\u0319',
'\u031C',
'\u031D',
'\u031E',
'\u031F',
'\u0320',
'\u0324',
'\u0325',
'\u0326',
'\u0329',
'\u032A',
'\u032B',
'\u032C',
'\u032D',
'\u032E',
'\u032F',
'\u0330',
'\u0331',
'\u0332',
'\u0333',
'\u0339',
'\u033A',
'\u033B',
'\u033C',
'\u0345',
'\u0347',
'\u0348',
'\u0349',
'\u034D',
'\u034E',
'\u0353',
'\u0354',
'\u0355',
'\u0356',
'\u0359',
'\u035A',
'\u0323',
];
}
/**
* Get a random character from an array
*/
randomChar(array) {
return array[Math.floor(Math.random() * array.length)];
}
/**
* Generate a specified number of random characters from an array
*/
randomChars(array, count) {
let result = '';
for (let i = 0; i < count; i++) {
result += this.randomChar(array);
}
return result;
}
/**
* Generate zalgo text with specified intensity
* @param {string} text - The input text to corrupt
* @param {number} intensity - Corruption intensity (1-10)
* @returns {string} - The corrupted zalgo text
*/
generate(text, intensity = 5) {
if (!text) return '';
// Normalize intensity to 1-10 range
intensity = Math.max(1, Math.min(10, intensity));
let result = '';
for (let char of text) {
result += char;
// Skip whitespace and newlines
if (/\s/.test(char)) {
continue;
}
// Calculate how many combining characters to add based on intensity
// Higher intensity = more corruption
const factor = intensity / 10;
// Add characters above (0-3 based on intensity)
const aboveCount = Math.floor(Math.random() * (4 * factor));
result += this.randomChars(this.above, aboveCount);
// Add characters in middle (0-2 based on intensity)
const middleCount = Math.floor(Math.random() * (3 * factor));
result += this.randomChars(this.middle, middleCount);
// Add characters below (0-3 based on intensity)
const belowCount = Math.floor(Math.random() * (4 * factor));
result += this.randomChars(this.below, belowCount);
}
return result;
}
/**
* Remove zalgo corruption from text
* @param {string} text - The corrupted text
* @returns {string} - Clean text without combining characters
*/
clean(text) {
// Remove all combining characters (Unicode category Mn)
return text.replace(/[\u0300-\u036f\u0489]/g, '');
}
}