diff --git a/app/morse-code/page.tsx b/app/morse-code/page.tsx new file mode 100644 index 00000000..ece5caab --- /dev/null +++ b/app/morse-code/page.tsx @@ -0,0 +1,22 @@ +import type { Metadata } from "next"; +import MorseCodeVisualizer from "../../components/encoding/MorseCodeVisualizer"; +import Navbar from "../../components/layout/Navbar"; +import Footer from "../../components/layout/footer"; + +export const metadata: Metadata = { + title: "Morse Code Visualizer — Encoder/Decoder | CryptoViz", + description: + "Encode and decode Morse code with real-time signal waveform visualization. Learn ITU International Morse Code with interactive timing analysis.", +}; + +export default function MorseCodePage() { + return ( +
+ +
+ +
+
+ ); +} diff --git a/components/encoding/MorseCodeVisualizer.tsx b/components/encoding/MorseCodeVisualizer.tsx new file mode 100644 index 00000000..6f1fc75d --- /dev/null +++ b/components/encoding/MorseCodeVisualizer.tsx @@ -0,0 +1,741 @@ +"use client"; + +import { useState, useMemo, useCallback, useRef, useEffect } from "react"; +import { + encodeMorse, + decodeMorse, + generateWaveform, + farnsworthTiming, + MORSE_TABLE, + type MorseResult, + type WaveformData, +} from "../../lib/encoding/morseCode"; +import { + Play, + Pause, + RotateCcw, + ArrowLeftRight, + Volume2, + VolumeX, + Copy, + Check, + Info, + Zap, + Clock, + Target, +} from "lucide-react"; +import { cn } from "../../lib/utils"; + +/* ─── Sample Messages ──────────────────────────────────────────────────────── */ + +const SAMPLES = [ + { label: "SOS", text: "SOS", hint: "The universal distress signal: ··· — — — ···" }, + { label: "HELLO WORLD", text: "HELLO WORLD", hint: "Classic first Morse message." }, + { label: "SHORT PHRASE", text: "THE QUICK BROWN FOX", hint: "Pangram in Morse." }, + { label: "NUMBERS", text: "12345", hint: "Morse digits 1-5." }, + { label: "PUNCTUATION", text: "STOP!", hint: "Including punctuation marks." }, +]; + +/* ─── Morse Reference Table ────────────────────────────────────────────────── */ + +function MorseRefTable() { + const letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".split(""); + const digits = "0123456789".split(""); + const [showAll, setShowAll] = useState(false); + const display = showAll ? letters : letters.slice(0, 13); + + return ( +
+
+

+ Letters +

+
+ {display.map((ch) => ( +
+ {ch} + + {MORSE_TABLE[ch]} + +
+ ))} +
+ {!showAll && ( + + )} +
+
+

+ Digits +

+
+ {digits.map((ch) => ( +
+ {ch} + + {MORSE_TABLE[ch]} + +
+ ))} +
+
+
+ ); +} + +/* ─── Waveform Renderer ────────────────────────────────────────────────────── */ + +function WaveformRenderer({ + waveform, + isPlaying, + playPosition, +}: { + waveform: WaveformData; + isPlaying: boolean; + playPosition: number; +}) { + const svgWidth = 800; + const svgHeight = 80; + const padding = { left: 10, right: 10, top: 15, bottom: 15 }; + const plotWidth = svgWidth - padding.left - padding.right; + const plotHeight = svgHeight - padding.top - padding.bottom; + const unitWidth = plotWidth / Math.max(waveform.totalDuration, 1); + + // Build SVG path for the waveform + const pathSegments: string[] = []; + let lastY = padding.top + plotHeight; // Start LOW + + // Start LOW + pathSegments.push(`M ${padding.left} ${padding.top + plotHeight}`); + + for (const [start, end] of waveform.highRanges) { + const x1 = padding.left + start * unitWidth; + const x2 = padding.left + end * unitWidth; + const highY = padding.top; + const lowY = padding.top + plotHeight; + + // Go HIGH + pathSegments.push(`L ${x1} ${lowY}`); + pathSegments.push(`L ${x1} ${highY}`); + // Stay HIGH + pathSegments.push(`L ${x2} ${highY}`); + // Go LOW + pathSegments.push(`L ${x2} ${lowY}`); + } + + // End LOW + pathSegments.push( + `L ${padding.left + waveform.totalDuration * unitWidth} ${padding.top + plotHeight}` + ); + + // Play position indicator + const playX = padding.left + playPosition * unitWidth; + + return ( +
+ + {/* Background grid */} + + + {/* Waveform path */} + + + {/* Filled area under waveform */} + + + {/* Gradient definition */} + + + + + + + + {/* Character labels at bottom */} + {waveform.charTimings.map((ct, i) => { + const x = + padding.left + + ((ct.startUnit + ct.endUnit) / 2) * unitWidth; + return ( + + {ct.char} + + ); + })} + + {/* Play position indicator */} + {isPlaying && ( + + )} + + + {/* Legend */} +
+ + + Signal HIGH + + + + Gap (LOW) + + 1 unit = dot duration +
+
+ ); +} + +/* ─── Character Breakdown ──────────────────────────────────────────────────── */ + +function CharacterBreakdown({ result }: { result: MorseResult }) { + if (result.characters.length === 0) return null; + + return ( +
+ + + + + + + + + + + {result.characters.slice(0, 30).map((mc, idx) => { + const signal = mc.code + .split("") + .map((s) => (s === "." ? "●" : s === "-" ? "━" : " ")) + .join(" "); + const units = mc.code + .split("") + .reduce((sum, s, i) => { + const val = s === "." ? 1 : s === "-" ? 3 : 0; + const gap = i < mc.code.length - 1 ? 1 : 0; + return sum + val + gap; + }, 0); + + return ( + + + + + + + ); + })} + +
CharMorseSignalUnits
+ {mc.char === " " ? "␣" : mc.char} + + {mc.code || "/"} + + {signal} + + {units || 7} +
+ {result.characters.length > 30 && ( +

+ Showing first 30 of {result.characters.length} characters +

+ )} +
+ ); +} + +/* ─── Timing Visual ────────────────────────────────────────────────────────── */ + +function TimingVisual({ wpm }: { wpm: number }) { + const timing = farnsworthTiming(wpm); + const items = [ + { label: "Dot (·)", ms: timing.dotMs, width: "1fr", color: "bg-teal-500" }, + { label: "Dash (—)", ms: timing.dashMs, width: "3fr", color: "bg-teal-400" }, + { label: "Intra-char gap", ms: timing.intraCharMs, width: "1fr", color: "bg-zinc-700" }, + { label: "Inter-char gap", ms: timing.interCharMs, width: "3fr", color: "bg-zinc-600" }, + { label: "Word gap", ms: timing.wordMs, width: "7fr", color: "bg-zinc-500" }, + ]; + + return ( +
+ {items.map((item) => ( +
+ + {item.label} + +
+
+
+ + {item.ms.toFixed(1)}ms + +
+ ))} +
+ ); +} + +/* ─── Main Component ──────────────────────────────────────────────────────── */ + +export default function MorseCodeVisualizer() { + const [mode, setMode] = useState<"encode" | "decode">("encode"); + const [inputText, setInputText] = useState("SOS"); + const [wpm, setWpm] = useState(20); + const [isPlaying, setIsPlaying] = useState(false); + const [playPosition, setPlayPosition] = useState(0); + const playRef = useRef(null); + const startTimeRef = useRef(0); + + const result: MorseResult | null = useMemo(() => { + if (!inputText.trim()) return null; + if (mode === "encode") { + return encodeMorse(inputText); + } else { + return decodeMorse(inputText); + } + }, [inputText, mode]); + + const waveform: WaveformData | null = useMemo(() => { + if (mode !== "encode" || !inputText.trim()) return null; + return generateWaveform(inputText); + }, [inputText, mode]); + + // Playback animation + useEffect(() => { + if (!isPlaying || !waveform) { + if (playRef.current) { + cancelAnimationFrame(playRef.current); + playRef.current = null; + } + return; + } + + startTimeRef.current = performance.now(); + const totalMs = (waveform.totalDuration / 50) * 1000 * (20 / wpm); + + const animate = (now: number) => { + const elapsed = now - startTimeRef.current; + const progress = Math.min(elapsed / totalMs, 1); + setPlayPosition(progress * waveform.totalDuration); + + if (progress < 1) { + playRef.current = requestAnimationFrame(animate); + } else { + setIsPlaying(false); + setPlayPosition(0); + } + }; + + playRef.current = requestAnimationFrame(animate); + + return () => { + if (playRef.current) cancelAnimationFrame(playRef.current); + }; + }, [isPlaying, waveform, wpm]); + + const handlePlay = useCallback(() => { + if (isPlaying) { + setIsPlaying(false); + setPlayPosition(0); + } else { + setPlayPosition(0); + setIsPlaying(true); + } + }, [isPlaying]); + + const handleCopy = useCallback(() => { + if (!result) return; + const text = mode === "encode" ? result.morse : result.decoded; + navigator.clipboard.writeText(text); + }, [result, mode]); + + const handleSample = useCallback( + (text: string) => { + setInputText(text); + setMode("encode"); + setIsPlaying(false); + setPlayPosition(0); + }, + [] + ); + + return ( +
+ {/* Header */} +
+
+
+

+ Encoding Tool +

+
+
+

+ Morse Code Visualizer +

+

+ Encode and decode International Morse Code with real-time signal waveform visualization. Learn timing patterns, character mappings, and the history of the world's first digital encoding system. +

+
+
+

+ Interactive Signal +

+

+ Watch the waveform render in real-time. Each dot is 1 time unit, each dash is 3 units. Adjust WPM to see how timing changes. +

+
+
+
+
+ + {/* Sample Messages */} +
+

+ Quick Examples +

+
+ {SAMPLES.map((sample) => ( + + ))} +
+
+ +
+ {/* Left Column: Input & Controls */} +
+ {/* Mode Toggle & Input */} +
+ {/* Mode Toggle */} +
+ + +
+ +

+ {mode === "encode" ? "Input Text" : "Input Morse Code"} +

+

+ {mode === "encode" + ? "Type text to encode into Morse code." + : "Paste Morse code (use spaces between characters, / for words)."} +

+
+