Created
March 14, 2026 12:12
-
-
Save nsdevaraj/48f7b4ef666484f6b319883005e4a053 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Morse Code Translator</title> | |
| <script src="https://unpkg.com/react@18/umd/react.production.min.js"></script> | |
| <script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script> | |
| <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script> | |
| <style> | |
| * { margin: 0; padding: 0; box-sizing: border-box; } | |
| body { font-family: 'Segoe UI', sans-serif; } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="root"></div> | |
| <script type="text/babel"> | |
| const { useState, useEffect, useRef, useCallback } = React; | |
| const MORSE = { | |
| 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:"--.." | |
| }; | |
| const N_ = { | |
| START:[460,32], | |
| T:[340,72],E:[560,72], | |
| M:[200,72],I:[680,72], | |
| O:[68,72],S:[790,72],H:[905,72], | |
| G:[228,205],U:[610,205], | |
| Q:[78,205],V:[755,205], | |
| Z:[268,335],F:[648,335], | |
| N:[368,455],A:[530,455], | |
| K:[208,455],R:[680,455], | |
| Y:[68,455],L:[832,455], | |
| C:[230,555], | |
| D:[388,645], | |
| X:[158,735],W:[548,735], | |
| P:[710,735], | |
| B:[318,835],J:[588,835], | |
| }; | |
| const EDGES = [ | |
| ["START","T","dash"],["START","E","dot"], | |
| ["T","M","dash"],["M","O","dash"], | |
| ["E","I","dot"],["I","S","dot"],["S","H","dot"], | |
| ["M","G","dot"],["G","Q","dash"],["G","Z","dot"], | |
| ["I","U","dash"],["U","F","dot"],["S","V","dash"], | |
| ["T","N","dot"],["E","A","dash"], | |
| ["N","K","dash"],["K","Y","dash"],["K","C","dot"], | |
| ["A","R","dot"],["R","L","dot"], | |
| ["N","D","dot"],["D","X","dash"],["D","B","dot"], | |
| ["A","W","dash"],["W","P","dot"],["W","J","dash"], | |
| ]; | |
| const childMap = {}; | |
| EDGES.forEach(([p,c,t]) => { | |
| if (!childMap[p]) childMap[p] = {}; | |
| childMap[p][t] = c; | |
| }); | |
| function getPath(letter) { | |
| const morse = MORSE[letter]; | |
| if (!morse) return []; | |
| const path = ["START"]; | |
| let cur = "START"; | |
| for (const s of morse) { | |
| const type = s === "." ? "dot" : "dash"; | |
| const next = childMap[cur]?.[type]; | |
| if (!next) break; | |
| path.push(next); | |
| cur = next; | |
| } | |
| return path; | |
| } | |
| function getActiveEdges(path) { | |
| const set = new Set(); | |
| for (let i = 0; i < path.length - 1; i++) { | |
| set.add(`${path[i]}-${path[i+1]}`); | |
| } | |
| return set; | |
| } | |
| function midXY(a, b) { | |
| return [(N_[a][0]+N_[b][0])/2, (N_[a][1]+N_[b][1])/2]; | |
| } | |
| const Rod = ({ x1,y1,x2,y2,glow }) => ( | |
| <line x1={x1} y1={y1} x2={x2} y2={y2} | |
| stroke={glow ? "#b45309" : "#3a3a3a"} | |
| strokeWidth={glow ? 5 : 4} | |
| strokeLinecap="round" | |
| /> | |
| ); | |
| const DotShape = ({ cx,cy,glow,vertical }) => ( | |
| <g> | |
| {glow && <circle cx={cx} cy={cy} r={16} fill="#f59e0b" opacity={0.2}/>} | |
| <circle cx={cx} cy={cy} r={11} | |
| fill={glow ? "url(#dotGlow)" : "url(#dotDark)"} | |
| stroke={glow ? "#fbbf24" : "#555"} strokeWidth={1.5}/> | |
| <ellipse cx={cx-3} cy={cy-3} rx={4} ry={3} fill="rgba(255,255,255,0.12)"/> | |
| </g> | |
| ); | |
| const DashShape = ({ cx,cy,glow,vertical }) => { | |
| const w = vertical ? 18 : 44; | |
| const h = vertical ? 44 : 18; | |
| return ( | |
| <g> | |
| {glow && <rect x={cx-w/2-3} y={cy-h/2-3} width={w+6} height={h+6} rx={6} fill="#f59e0b" opacity={0.18}/>} | |
| <rect x={cx-w/2} y={cy-h/2} width={w} height={h} rx={4} | |
| fill={glow ? "url(#dashGlow)" : "url(#dashDark)"} | |
| stroke={glow ? "#fbbf24" : "#555"} strokeWidth={1.5}/> | |
| <rect x={cx-w/2+3} y={cy-h/2+3} width={vertical?12:26} height={vertical?12:5} rx={2} fill="rgba(255,255,255,0.08)"/> | |
| </g> | |
| ); | |
| }; | |
| const NodeCircle = ({ cx,cy,glow,label,labelPos }) => { | |
| const off = labelPos || "below"; | |
| let lx=cx, ly=cy; | |
| if (off==="below") { ly=cy+22; } | |
| else if (off==="above") { ly=cy-16; } | |
| else if (off==="left") { lx=cx-18; ly=cy+2; } | |
| else if (off==="right") { lx=cx+18; ly=cy+2; } | |
| return ( | |
| <g> | |
| {glow && <circle cx={cx} cy={cy} r={14} fill="#f59e0b" opacity={0.25}/>} | |
| <circle cx={cx} cy={cy} r={8} | |
| fill={glow ? "#92400e" : "#222"} | |
| stroke={glow ? "#fbbf24" : "#555"} strokeWidth={1.5}/> | |
| {glow && <circle cx={cx} cy={cy} r={4} fill="#fde68a" opacity={0.6}/>} | |
| <text x={lx} y={ly} textAnchor="middle" dominantBaseline="central" | |
| fontSize={16} fontWeight={800} fontFamily="'Courier New',monospace" | |
| fill={glow ? "#fde68a" : "#777"} | |
| style={glow ? {filter:"url(#textGlow)"} : {}}> | |
| {label} | |
| </text> | |
| </g> | |
| ); | |
| }; | |
| const labelPositions = { | |
| O:"above",M:"above",T:"above",E:"above",I:"above",S:"above",H:"above", | |
| Q:"left",G:"right",U:"left",V:"right", | |
| Z:"right",F:"right", | |
| Y:"above",K:"above",N:"above",A:"above",R:"above",L:"above", | |
| C:"right",D:"right",X:"left",W:"left",P:"right", | |
| B:"right",J:"right" | |
| }; | |
| function MorseTranslator() { | |
| const [text, setText] = useState(""); | |
| const [charIdx, setCharIdx] = useState(0); | |
| const [playing, setPlaying] = useState(false); | |
| const [playIdx, setPlayIdx] = useState(-1); | |
| const stopRef = useRef(false); | |
| const audioCtx = useRef(null); | |
| const chars = text.toUpperCase().split("").filter(c => MORSE[c] || c===" "); | |
| const currentChar = playing ? (chars[playIdx] || "") : (chars[charIdx] || ""); | |
| const path = currentChar && currentChar!==" " ? getPath(currentChar) : []; | |
| const activeNodes = new Set(path); | |
| const activeEdges = getActiveEdges(path); | |
| const getAudioCtx = () => { | |
| if (!audioCtx.current) audioCtx.current = new (window.AudioContext || window.webkitAudioContext)(); | |
| return audioCtx.current; | |
| }; | |
| const beep = (dur) => new Promise(res => { | |
| const ctx = getAudioCtx(); | |
| const o = ctx.createOscillator(); | |
| const g = ctx.createGain(); | |
| o.frequency.value = 620; o.type = "sine"; g.gain.value = 0.12; | |
| o.connect(g); g.connect(ctx.destination); | |
| o.start(); setTimeout(() => { o.stop(); res(); }, dur); | |
| }); | |
| const sleep = ms => new Promise(r => setTimeout(r, ms)); | |
| const playMorse = async () => { | |
| if (playing) { stopRef.current = true; return; } | |
| if (chars.length === 0) return; | |
| stopRef.current = false; | |
| setPlaying(true); | |
| for (let i = 0; i < chars.length; i++) { | |
| if (stopRef.current) break; | |
| setPlayIdx(i); | |
| const c = chars[i]; | |
| const m = MORSE[c]; | |
| if (!m) { await sleep(400); continue; } | |
| for (const s of m) { | |
| if (stopRef.current) break; | |
| await beep(s === "." ? 80 : 240); | |
| await sleep(80); | |
| } | |
| await sleep(350); | |
| } | |
| setPlayIdx(-1); | |
| setPlaying(false); | |
| }; | |
| const handleInput = (e) => { | |
| setText(e.target.value); | |
| setCharIdx(0); | |
| }; | |
| const morseStr = currentChar && MORSE[currentChar] ? MORSE[currentChar] : ""; | |
| const btnStyle = (bg,color) => ({ | |
| background:bg,border:"1px solid #444",borderRadius:6,padding:"6px 16px", | |
| color,fontSize:12,fontWeight:700,cursor:"pointer",letterSpacing:1, | |
| textTransform:"uppercase",fontFamily:"'Courier New',monospace", | |
| }); | |
| return ( | |
| <div style={{ | |
| minHeight:"100vh", | |
| background:"linear-gradient(160deg,#0a0a0a 0%,#151515 40%,#0e0e0e 100%)", | |
| color:"#ccc",fontFamily:"'Segoe UI',sans-serif", | |
| padding:"16px 8px",boxSizing:"border-box", | |
| }}> | |
| <div style={{maxWidth:720,margin:"0 auto"}}> | |
| <div style={{textAlign:"center",marginBottom:12}}> | |
| <h1 style={{fontSize:20,fontWeight:800,color:"#ddd",margin:0,letterSpacing:6,fontFamily:"'Courier New',monospace",textTransform:"uppercase"}}> | |
| Morse Code Translator | |
| </h1> | |
| <div style={{width:50,height:2,margin:"6px auto",background:"linear-gradient(90deg,transparent,#f59e0b,transparent)"}}/> | |
| </div> | |
| <div style={{background:"#111",borderRadius:12,border:"1px solid #252525",padding:"8px",marginBottom:10,boxShadow:"inset 0 4px 20px rgba(0,0,0,0.7)"}}> | |
| <svg viewBox="0 0 960 890" width="100%" style={{display:"block"}}> | |
| <defs> | |
| <radialGradient id="dotGlow" cx="50%" cy="40%" r="60%"> | |
| <stop offset="0%" stopColor="#fde68a"/> | |
| <stop offset="60%" stopColor="#f59e0b"/> | |
| <stop offset="100%" stopColor="#92400e"/> | |
| </radialGradient> | |
| <radialGradient id="dotDark" cx="35%" cy="35%" r="65%"> | |
| <stop offset="0%" stopColor="#444"/> | |
| <stop offset="100%" stopColor="#1a1a1a"/> | |
| </radialGradient> | |
| <linearGradient id="dashGlow" x1="0" y1="0" x2="0" y2="1"> | |
| <stop offset="0%" stopColor="#fde68a"/> | |
| <stop offset="50%" stopColor="#f59e0b"/> | |
| <stop offset="100%" stopColor="#92400e"/> | |
| </linearGradient> | |
| <linearGradient id="dashDark" x1="0" y1="0" x2="0" y2="1"> | |
| <stop offset="0%" stopColor="#444"/> | |
| <stop offset="50%" stopColor="#2a2a2a"/> | |
| <stop offset="100%" stopColor="#1a1a1a"/> | |
| </linearGradient> | |
| <filter id="textGlow"> | |
| <feGaussianBlur stdDeviation="3" result="blur"/> | |
| <feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge> | |
| </filter> | |
| <filter id="lineGlow"> | |
| <feGaussianBlur stdDeviation="4" result="blur"/> | |
| <feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge> | |
| </filter> | |
| </defs> | |
| <rect width="960" height="890" fill="#0f0f0f" rx="8"/> | |
| {EDGES.map(([p,c,t],i) => { | |
| const [x1,y1] = N_[p]; | |
| const [x2,y2] = N_[c]; | |
| const key = `${p}-${c}`; | |
| const glow = activeEdges.has(key); | |
| return ( | |
| <g key={i} style={glow ? {filter:"url(#lineGlow)"} : {}}> | |
| <Rod x1={x1} y1={y1} x2={x2} y2={y2} glow={glow}/> | |
| </g> | |
| ); | |
| })} | |
| {EDGES.map(([p,c,t],i) => { | |
| const [mx,my] = midXY(p,c); | |
| const key = `${p}-${c}`; | |
| const glow = activeEdges.has(key); | |
| const vertical = Math.abs(N_[p][1]-N_[c][1]) > Math.abs(N_[p][0]-N_[c][0]); | |
| if (t === "dot") return <DotShape key={`s${i}`} cx={mx} cy={my} glow={glow} vertical={vertical}/>; | |
| return <DashShape key={`s${i}`} cx={mx} cy={my} glow={glow} vertical={vertical}/>; | |
| })} | |
| {(() => { | |
| const [sx,sy] = N_.START; | |
| const glow = activeNodes.has("START"); | |
| return ( | |
| <g> | |
| <polygon points={`${sx-12},${sy-16} ${sx+12},${sy-16} ${sx},${sy+4}`} | |
| fill={glow?"#92400e":"#2a2a2a"} stroke={glow?"#fbbf24":"#555"} strokeWidth={1.5}/> | |
| {glow && <polygon points={`${sx-12},${sy-16} ${sx+12},${sy-16} ${sx},${sy+4}`} | |
| fill="#f59e0b" opacity={0.3}/>} | |
| </g> | |
| ); | |
| })()} | |
| {Object.entries(N_).filter(([k])=>k!=="START").map(([letter,[cx,cy]]) => ( | |
| <NodeCircle key={letter} cx={cx} cy={cy} | |
| glow={activeNodes.has(letter)} | |
| label={letter} | |
| labelPos={labelPositions[letter]}/> | |
| ))} | |
| </svg> | |
| </div> | |
| <div style={{textAlign:"center",marginBottom:8,minHeight:50}}> | |
| {currentChar && currentChar!==" " && ( | |
| <div style={{display:"inline-flex",alignItems:"center",gap:12,background:"rgba(245,158,11,0.08)",border:"1px solid rgba(245,158,11,0.2)",borderRadius:10,padding:"8px 20px"}}> | |
| <span style={{fontSize:28,fontWeight:900,fontFamily:"'Courier New',monospace",color:"#fde68a"}}>{currentChar}</span> | |
| <span style={{fontSize:20,fontFamily:"monospace",color:"#f59e0b",letterSpacing:4}}>{morseStr}</span> | |
| <span style={{fontSize:13,color:"#888",fontFamily:"monospace"}}> | |
| {morseStr.split("").map(s=>s==="."?"dot":"dash").join(" -> ")} | |
| </span> | |
| </div> | |
| )} | |
| </div> | |
| <div style={{background:"#131313",borderRadius:10,border:"1px solid #222",padding:12}}> | |
| <input type="text" value={text} onChange={handleInput} placeholder="Type a message..." | |
| maxLength={40} | |
| style={{ | |
| width:"100%",background:"#0a0a0a",border:"1px solid #333",borderRadius:8, | |
| padding:"10px 14px",fontSize:18,fontFamily:"'Courier New',monospace", | |
| color:"#fde68a",letterSpacing:3,outline:"none",boxSizing:"border-box", | |
| caretColor:"#f59e0b", | |
| }}/> | |
| <div style={{display:"flex",gap:8,marginTop:10,justifyContent:"center",flexWrap:"wrap",alignItems:"center"}}> | |
| {chars.length > 1 && !playing && ( | |
| <> | |
| <button onClick={()=>setCharIdx(Math.max(0,charIdx-1))} style={btnStyle("#1a1a1a","#666")}>◀ Prev</button> | |
| <span style={{color:"#888",fontSize:13,fontFamily:"monospace",minWidth:60,textAlign:"center"}}> | |
| {charIdx+1} / {chars.length} | |
| </span> | |
| <button onClick={()=>setCharIdx(Math.min(chars.length-1,charIdx+1))} style={btnStyle("#1a1a1a","#666")}>Next ▶</button> | |
| <div style={{width:1,height:24,background:"#333",margin:"0 4px"}}/> | |
| </> | |
| )} | |
| <button onClick={playMorse} style={btnStyle(playing?"#7f1d1d":"#78350f",playing?"#fca5a5":"#fde68a")}> | |
| {playing ? "Stop" : "Transmit"} | |
| </button> | |
| <button onClick={()=>{setText("");setCharIdx(0);}} style={btnStyle("#1a1a1a","#888")}>Clear</button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| ReactDOM.createRoot(document.getElementById('root')).render(<MorseTranslator />); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment