SEMAPHORE - INTERACTIVE APP SOURCE CODE
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Translate to Semaphore</title>
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script src="https://cdn.tailwindcss.com"></script>
<style>
body {
margin: 0;
padding: 0;
background-color: #000;
color: #fff;
overflow-x: hidden;
}
.flag-animation {
transition: transform 0.1s ease;
}
.semaphore-flag {
filter: drop-shadow(0 1px 1px rgba(0, 0, 0, 0.8));
}
.status-indicator {
height: 8px;
width: 8px;
border-radius: 50%;
display: inline-block;
margin-right: 8px;
}
.status-active {
background-color: #fff;
box-shadow: 0 0 4px #fff;
}
.status-inactive {
background-color: #666;
}
.flag-display-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(60px, 1fr));
gap: 6px;
}
@media (max-width: 640px) {
.flag-display-grid {
grid-template-columns: repeat(auto-fill, minmax(52px, 1fr));
gap: 5px;
}
}
.flag-item {
aspect-ratio: 1;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid #333;
background: #111;
transition: all 0.1s ease;
}
.flag-item-active {
background: #1a1a1a;
border-color: #666;
}
.flag-item-inactive {
opacity: 0.3;
filter: grayscale(0.8);
}
.btn-primary {
background: #fff;
color: #000;
border: none;
font-weight: 500;
transition: all 0.2s;
}
.btn-primary:hover:not(:disabled) {
background: #e6e6e6;
}
.btn-primary:disabled {
background: #333;
color: #666;
cursor: not-allowed;
}
.btn-secondary {
background: transparent;
color: #fff;
border: 1px solid #333;
font-weight: 500;
transition: all 0.2s;
}
.btn-secondary:hover {
background: #1a1a1a;
border-color: #666;
}
.input-field {
background: #111;
border: 1px solid #333;
color: #fff;
transition: all 0.2s;
font-family: monospace;
font-size: 14px;
line-height: 1.4;
}
.input-field:focus {
outline: none;
border-color: #666;
background: #0a0a0a;
}
.container-border {
border: 1px solid #333;
background: #0a0a0a;
}
.input-field::-webkit-scrollbar {
width: 8px;
}
.input-field::-webkit-scrollbar-track {
background: #111;
}
.input-field::-webkit-scrollbar-thumb {
background: #333;
border-radius: 0;
}
.input-field::-webkit-scrollbar-thumb:hover {
background: #444;
}
.input-field {
scrollbar-width: thin;
scrollbar-color: #333 #111;
}
.text-input {
resize: vertical;
min-height: 100px;
max-height: 300px;
overflow-y: auto;
}
</style>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
const { useState, useEffect } = React;
const SemaphoreTranslator = () => {
const defaultText = "Semaphore is a hand held signalling system used in a visual telegraphy to communicate messages over distances with two flags";
const [inputText, setInputText] = useState(defaultText);
const [isAnimating, setIsAnimating] = useState(false);
const [currentIndex, setCurrentIndex] = useState(0);
const [displayText, setDisplayText] = useState('');
const semaphorePositions = {
'a': [225, 180],
'b': [225, 135],
'c': [225, 90],
'd': [225, 45],
'e': [180, 45],
'f': [180, 90],
'g': [180, 135],
'h': [135, 90],
'i': [90, 135],
'j': [0, 180],
'k': [180, 225],
'l': [135, 225],
'm': [90, 45],
'n': [45, 90],
'o': [315, 135],
'p': [315, 90],
'q': [315, 45],
'r': [135, 180],
's': [90, 180],
't': [90, 225],
'u': [135, 315],
'v': [225, 0],
'w': [315, 225],
'x': [315, 180],
'y': [135, 45],
'z': [90, 315],
' ': [0, 0]
};
useEffect(() => {
if (isAnimating && currentIndex < displayText.length) {
const timer = setTimeout(() => {
setCurrentIndex(currentIndex + 1);
}, 200);
return () => clearTimeout(timer);
} else if (currentIndex >= displayText.length && isAnimating) {
setIsAnimating(false);
}
}, [currentIndex, isAnimating, displayText]);
const handleTranslate = () => {
const cleaned = inputText.toLowerCase().replace(/[^a-z ]/g, '');
setDisplayText(cleaned);
setCurrentIndex(0);
setIsAnimating(true);
};
const handleClear = () => {
setInputText('');
setDisplayText('');
setCurrentIndex(0);
setIsAnimating(false);
};
const handleKeyPress = (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleTranslate();
}
};
const renderFlag = (angle, side) => {
const cx = 50;
const cy = 50;
const length = 35;
const rad = (angle * Math.PI) / 180;
const x = cx + length * Math.cos(rad);
const y = cy + length * Math.sin(rad);
const flagLength = 12;
const flagWidth = 8;
const flagRad = rad - Math.PI / 2;
return (
<g className="flag-animation">
<line
x1={cx}
y1={cy}
x2={x}
y2={y}
stroke="#666"
strokeWidth="1.5"
/>
<rect
x={x}
y={y}
width={flagWidth}
height={flagLength}
fill={side === 'left' ? '#2563eb' : '#dc2626'}
transform={`rotate(${angle + 90} ${x} ${y})`}
/>
</g>
);
};
const renderSemaphore = (char) => {
const position = semaphorePositions[char] || [0, 0];
return (
<svg width="100%" height="100%" viewBox="0 0 100 100" className="semaphore-flag">
<circle cx="50" cy="50" r="3" fill="#666" />
{renderFlag(position[0], 'left')}
{renderFlag(position[1], 'right')}
</svg>
);
};
return (
<div className="min-h-screen bg-black text-white p-4 md:p-6">
<div className="max-w-6xl mx-auto">
<div className="flex flex-col md:flex-row gap-4 mb-6">
<div className="flex-1">
<textarea
value={inputText}
onChange={(e) => setInputText(e.target.value)}
onKeyPress={handleKeyPress}
placeholder="Enter text to translate"
className="w-full input-field text-input rounded px-4 py-3 text-sm"
/>
</div>
<div className="flex gap-3">
<button
onClick={handleTranslate}
disabled={!inputText.trim() || isAnimating}
className="btn-primary px-6 py-3 rounded text-sm font-medium min-w-[120px]"
>
{isAnimating ? 'TRANSLATING' : 'TRANSLATE'}
</button>
<button
onClick={handleClear}
className="btn-secondary px-6 py-3 rounded text-sm font-medium"
>
CLEAR
</button>
</div>
</div>
<div className="container-border rounded p-4 mb-4">
<div className="flex justify-between items-center mb-4">
<div className="flex items-center">
<span className={`status-indicator ${isAnimating ? 'status-active' : 'status-inactive'}`}></span>
<span className="text-sm font-medium">
{isAnimating
? `TRANSMITTING: ${currentIndex}/${displayText.length}`
: displayText
? 'TRANSMISSION COMPLETE'
: 'AWAITING INPUT'
}
</span>
</div>
{displayText && (
<div className="text-xs text-gray-400">
{displayText.length} CHARACTER{displayText.length !== 1 ? 'S' : ''}
</div>
)}
</div>
<div className="flag-display-grid">
{displayText ? (
displayText.split('').map((char, idx) => (
<div
key={idx}
className={`flag-item rounded ${idx < currentIndex ? 'flag-item-active' : 'flag-item-inactive'}`}
>
{idx < currentIndex ? renderSemaphore(char) : (
<div className="w-full h-full flex items-center justify-center">
<div className="w-2 h-2 rounded-full bg-gray-800"></div>
</div>
)}
</div>
))
) : (
<div className="col-span-full flex flex-col items-center justify-center py-12">
<svg className="w-16 h-16 mb-4 opacity-20" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1" d="M3 21v-4m0 0V5a2 2 0 012-2h6.5l1 1H21l-3 6 3 6h-8.5l-1-1H5a2 2 0 00-2 2zm9-13.5V9"></path>
</svg>
<p className="text-gray-500 text-sm">ENTER TEXT TO SEE SEMAPHORE FLAGS</p>
</div>
)}
</div>
</div>
<div className="text-xs text-gray-500 mt-8 pt-4 border-t border-gray-800">
<div className="flex flex-wrap gap-6">
<div className="flex items-center gap-2">
<div className="w-3 h-3 border border-gray-600"></div>
<span>INACTIVE SIGNAL</span>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-3 bg-gray-800 border border-gray-500"></div>
<span>ACTIVE SIGNAL</span>
</div>
<div className="flex items-center gap-2">
<div className="w-2 h-4 bg-blue-600"></div>
<span>LEFT FLAG</span>
</div>
<div className="flex items-center gap-2">
<div className="w-2 h-4 bg-red-600"></div>
<span>RIGHT FLAG</span>
</div>
</div>
</div>
</div>
</div>
);
};
const container = document.getElementById('root');
const root = ReactDOM.createRoot(container);
root.render(<SemaphoreTranslator />);
</script>
</body>
</html>