Skip to content

Instantly share code, notes, and snippets.

@nsdevaraj
Created March 14, 2026 12:12
Show Gist options
  • Select an option

  • Save nsdevaraj/48f7b4ef666484f6b319883005e4a053 to your computer and use it in GitHub Desktop.

Select an option

Save nsdevaraj/48f7b4ef666484f6b319883005e4a053 to your computer and use it in GitHub Desktop.
<!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")}>&#9664; 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 &#9654;</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