From 26b4baa374f6d88b9653ecd0ae10a3cb8574c13b Mon Sep 17 00:00:00 2001 From: karan-chaos Date: Fri, 28 Aug 2026 17:02:00 +0530 Subject: [PATCH] feat(encoding): add Morse Code Visualizer with signal waveform MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Interactive Morse code encoder/decoder with real-time SVG waveform rendering, Farnsworth timing analysis, character breakdown table, ITU standard reference, and playback simulation. Includes encode/decode modes, WPM speed control, and 30+ unit tests. ๐Ÿค– Generated with Codebuff Co-Authored-By: Codebuff --- app/morse-code/page.tsx | 22 + components/encoding/MorseCodeVisualizer.tsx | 741 ++++++++++++++++++++ lib/encoding/morseCode.ts | 391 +++++++++++ tests/unit/morseCode.test.ts | 283 ++++++++ 4 files changed, 1437 insertions(+) create mode 100644 app/morse-code/page.tsx create mode 100644 components/encoding/MorseCodeVisualizer.tsx create mode 100644 lib/encoding/morseCode.ts create mode 100644 tests/unit/morseCode.test.ts 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)."} +

+
+