• 01 - Critter Models 02
  • 02 - VantaJS Shaders
  • 03 - App Bundle 02
  • 04 - Cathedrals
  • 05 - App Bundle 01
  • 06 - Retroscopic
  • 07 - Games
  • 08 - Voronoi
  • 09 - Terrain
  • 10 - Critter Models 01
  • 11 - Chromatophores
  • 12 - Conformal Maps
  • 13 - Echo Logix
  • 14 - Music
  • 15 - Colour Theory
MIKE'S NOTEBOOK
Notes
Number:
Project:
01Critter Models 02
└ Locust└ Butterfly└ Cockroach└ Spider└ Scorpion└ Dragon└ Brazilian Treehopper└ Tardigrade└ Diatoms└ Bumblebee└ Wasp└ Hammerhead Worm└ Giraffe Weevil└ Trilobite Beetle
02VantaJS Shaders
└ Clouds└ Waves└ Cells└ Rings└ Halo└ Globe└ Net└ Dots└ Birds└ Trunk└ Topology└ About VantaJS
03App Bundle 02
└ Gutindex└ Insect Builder└ OpenLayers
04Cathedrals
└ Lattice└ Lost└ Lucent└ Lumen└ Glass└ Disco└ Hyperion└ Latent
05App Bundle 01
└ Robohash└ PrismJS└ Horoscope└ Binary Jazz└ Morse Code└ URL Extractor└ Entity Escaper└ Phonetic Alphabet└ Toki Pona└ Semaphore└ Cut Up Machine└ Text Mixer└ Currency Freaks└ Host IO└ CrUX API└ Logo Dev└ Web Speech API└ GEO DB
06Retroscopic
└ Galaxy└ Ocean└ Slime└ 24-Cell└ Unicorn└ Polyp└ Oneiric 1└ Oneiric 2└ Node└ Torus Knot└ GLSL Grid└ Liquid Crystal└ Silky└ Butterfly└ Ocean 2└ Icosahedron└ Luma└ Swarm└ Broken Solid└ Hypersphere└ Constraints└ Psycho Killer
01 / 03

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>
MikeHiggins.me.uk LinkedIn GitHub Bluesky Contra Upwork
© 2026 Mike Higgins. All rights reserved.