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>
122
app/app.js
Normal 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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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, '');
|
||||
}
|
||||
}
|
||||