← Mike Higgins Home
Custom Code Experiment •

Morse Code

Source Code View HTML / CSS / JavaScript
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Morse Code</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
            background: #000000;
            min-height: 100vh;
            display: flex;
            justify-content: center;
            align-items: center;
            padding: 0;
            margin: 0;
        }

        .container {
            background: #000000;
            width: 100%;
            min-height: 100vh;
            display: flex;
            flex-direction: column;
            padding: 20px;
            max-width: none;
            margin: 0;
        }

        h1 {
            text-align: center;
            color: #fff;
            margin-bottom: 30px;
            font-size: 1.8em;
            font-weight: 600;
        }

        .input-section {
            margin-bottom: 20px;
            flex: 0 0 auto;
        }

        label {
            display: block;
            margin-bottom: 8px;
            color: #8b92b0;
            font-weight: 500;
            font-size: 14px;
        }

        textarea {
            width: 100%;
            padding: 16px 20px;
            border: 1px solid #333;
            border-radius: 8px;
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
            font-size: 16px;
            resize: vertical;
            min-height: 120px;
            background: #000000;
            color: #e4e7f1;
            transition: border-color 0.2s;
            line-height: 1.5;
        }

        textarea:focus {
            outline: none;
            border-color: #4a7cff;
        }

        textarea::placeholder {
            color: #666;
        }

        button {
            width: 100%;
            padding: 16px;
            background: #4a7cff;
            color: white;
            border: none;
            border-radius: 8px;
            font-size: 16px;
            font-weight: 600;
            cursor: pointer;
            transition: background 0.2s;
            flex: 0 0 auto;
            margin: 10px 0;
        }

        button:hover {
            background: #3d6ae6;
        }

        button:disabled {
            background: #333;
            cursor: not-allowed;
            opacity: 0.5;
        }

        .output-section {
            flex: 1;
            display: flex;
            flex-direction: column;
            min-height: 0;
        }

        .morse-output {
            background: #000000;
            border: 1px solid #333;
            border-radius: 8px;
            padding: 20px;
            flex: 1;
            font-size: 16px;
            letter-spacing: 4px;
            word-wrap: break-word;
            color: #4a7cff;
            font-family: 'Courier New', Courier, monospace;
            overflow-y: auto;
            line-height: 1.8;
            white-space: pre-wrap;
        }

        .status {
            margin-top: 12px;
            text-align: center;
            font-size: 14px;
            color: #8b92b0;
            min-height: 20px;
            flex: 0 0 auto;
        }

        .status.playing {
            color: #4a7cff;
            font-weight: 600;
        }

        @media (min-width: 768px) {
            .container {
                padding: 40px;
                max-width: 100%;
                width: 100%;
            }

            textarea {
                min-height: 150px;
                font-size: 18px;
                padding: 20px 24px;
            }

            .morse-output {
                font-size: 18px;
                padding: 24px;
                letter-spacing: 5px;
            }

            button {
                padding: 18px;
                font-size: 18px;
                margin: 20px 0;
            }

            h1 {
                font-size: 2.5em;
                margin-bottom: 40px;
            }

            label {
                font-size: 16px;
            }

            .status {
                font-size: 15px;
            }
        }

        @media (min-width: 1200px) {
            .container {
                padding: 60px;
                max-width: 1400px;
                margin: 0 auto;
            }

            textarea {
                min-height: 180px;
                font-size: 20px;
                padding: 24px 30px;
            }

            .morse-output {
                font-size: 20px;
                padding: 30px;
                letter-spacing: 6px;
            }

            button {
                padding: 20px;
                font-size: 20px;
                margin: 30px 0;
            }

            h1 {
                font-size: 3em;
                margin-bottom: 50px;
            }
        }

        @media (max-width: 480px) {
            .container {
                padding: 15px;
            }

            textarea {
                min-height: 100px;
                padding: 12px 16px;
                font-size: 15px;
            }

            .morse-output {
                padding: 16px;
                font-size: 15px;
                letter-spacing: 3px;
            }

            button {
                padding: 14px;
                font-size: 15px;
            }

            h1 {
                font-size: 1.5em;
                margin-bottom: 20px;
            }
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="input-section">
            <label for="textInput">Enter your message:</label>
            <textarea id="textInput" placeholder="Type your message here..."></textarea>
        </div>

        <button id="translateBtn">Translate & Play</button>

        <div class="output-section">
            <label>Morse Code Output:</label>
            <div class="morse-output" id="morseOutput">Your morse code will appear here...</div>
            <div class="status" id="status"></div>
        </div>
    </div>

    <script>
        const morseCode = {
            'A': '.-', 'B': '-...', 'C': '-.-.', 'D': '-..', 'E': '.',
            'F': '..-.', 'G': '--.', 'H': '....', 'I': '..', 'J': '.---',
            'K': '-.-', 'L': '.-..', 'M': '--', 'N': '-.', 'O': '---',
            'P': '.--.', 'Q': '--.-', 'R': '.-.', 'S': '...', 'T': '-',
            'U': '..-', 'V': '...-', 'W': '.--', 'X': '-..-', 'Y': '-.--',
            'Z': '--..', '0': '-----', '1': '.----', '2': '..---', '3': '...--',
            '4': '....-', '5': '.....', '6': '-....', '7': '--...', '8': '---..',
            '9': '----.', '.': '.-.-.-', ',': '--..--', '?': '..--..',
            "'": '.----.', '!': '-.-.--', '/': '-..-.', '(': '-.--.',
            ')': '-.--.-', '&': '.-...', ':': '---...', ';': '-.-.-.',
            '=': '-...-', '+': '.-.-.', '-': '-....-', '_': '..--.-',
            '"': '.-..-.', '$': '...-..-', '@': '.--.-.'
        };

        const audioContext = new (window.AudioContext || window.webkitAudioContext)();
        const dotDuration = 80;
        const dashDuration = dotDuration * 3;
        const symbolGap = dotDuration;
        const letterGap = dotDuration * 3;
        const wordGap = dotDuration * 7;
        const frequency = 600;

        let isPlaying = false;

        function playTone(duration) {
            return new Promise(resolve => {
                const oscillator = audioContext.createOscillator();
                const gainNode = audioContext.createGain();

                oscillator.connect(gainNode);
                gainNode.connect(audioContext.destination);

                oscillator.frequency.value = frequency;
                oscillator.type = 'sine';

                gainNode.gain.setValueAtTime(0.3, audioContext.currentTime);
                gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + duration / 1000);

                oscillator.start(audioContext.currentTime);
                oscillator.stop(audioContext.currentTime + duration / 1000);

                setTimeout(resolve, duration);
            });
        }

        function sleep(ms) {
            return new Promise(resolve => setTimeout(resolve, ms));
        }

        async function playMorseCode(text) {
            const morseOutput = document.getElementById('morseOutput');
            const status = document.getElementById('status');
            const translateBtn = document.getElementById('translateBtn');

            isPlaying = true;
            translateBtn.disabled = true;
            morseOutput.textContent = '';
            status.textContent = 'Playing...';
            status.className = 'status playing';

            const words = text.toUpperCase().split(' ');

            for (let w = 0; w < words.length; w++) {
                const word = words[w];

                for (let i = 0; i < word.length; i++) {
                    const char = word[i];
                    const morse = morseCode[char];

                    if (morse) {
                        for (let j = 0; j < morse.length; j++) {
                            const symbol = morse[j];

                            if (symbol === '.') {
                                morseOutput.textContent += '.';
                                await playTone(dotDuration);
                            } else if (symbol === '-') {
                                morseOutput.textContent += '-';
                                await playTone(dashDuration);
                            }

                            if (j < morse.length - 1) {
                                await sleep(symbolGap);
                            }
                        }

                        if (i < word.length - 1) {
                            morseOutput.textContent += ' ';
                            await sleep(letterGap);
                        }
                    }
                }

                if (w < words.length - 1) {
                    morseOutput.textContent += '   ';
                    await sleep(wordGap);
                }
            }

            status.textContent = 'Complete!';
            status.className = 'status';
            translateBtn.disabled = false;
            isPlaying = false;
        }

        document.getElementById('translateBtn').addEventListener('click', () => {
            const text = document.getElementById('textInput').value.trim();

            if (!text) {
                alert('Please enter some text to translate!');
                return;
            }

            if (isPlaying) {
                return;
            }

            playMorseCode(text);
        });

        document.getElementById('textInput').addEventListener('keypress', (e) => {
            if (e.key === 'Enter' && e.ctrlKey) {
                document.getElementById('translateBtn').click();
            }
        });
    </script>
</body>
</html>