F4HXN

Générateur de Code Morse réalisé en HTML

Publié le 22 décembre 2023 Par F4HXN

Traducteur de Code Morse — F4HXN

Traducteur de code Morse

Saisissez votre texte : la traduction s’affiche en direct, et vous pouvez l’écouter à la vitesse de votre choix.

Vitesse 20 WPM
Tonalité 600 Hz

Chronologie standard PARIS : point = 1 temps, trait = 3 temps, espace entre signes = 1 temps, entre lettres = 3 temps, entre mots = 7 temps. À 20 WPM, un temps dure 60 ms.

Le code source


<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="author" content="F4HXN">
<meta name="description" content="Traducteur de code Morse avec lecture audio a vitesse reglable (WPM), ponctuation et chiffres - F4HXN">
<meta name="copyright" content="CC0 1.0 Universel">
<title>Traducteur de Code Morse — F4HXN</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet">
<style>
  :root{
    --bleu:#0076a5;
    --bleu-clair:#298ad4;
    --encre:#0f1e28;
    --gris:#5a6b75;
    --trait:#e2e8ec;
    --fond-doux:#f5f8fa;
    --blanc:#ffffff;
  }
  *{box-sizing:border-box}
  .fmorse{
    font-family:"DM Sans",system-ui,sans-serif;
    color:var(--encre);
    max-width:1200px;
    margin:0 auto;
    line-height:1.6;
  }
  .fmorse .fm-wrap{
    background:var(--blanc);
    border:1px solid var(--trait);
    border-radius:14px;
    padding:28px;
  }
  .fmorse h2.fm-titre{
    font-size:1.5rem;
    font-weight:700;
    margin:0 0 4px;
    color:var(--encre);
    letter-spacing:-.01em;
  }
  .fmorse .fm-sous{
    margin:0 0 22px;
    color:var(--gris);
    font-size:.95rem;
  }
  .fmorse label{
    display:block;
    font-size:.82rem;
    font-weight:600;
    text-transform:uppercase;
    letter-spacing:.05em;
    color:var(--bleu);
    margin:0 0 8px;
  }
  .fmorse textarea{
    width:100%;
    min-height:96px;
    resize:vertical;
    font-family:"DM Sans",sans-serif;
    font-size:1.05rem;
    line-height:1.5;
    color:var(--encre);
    background:var(--blanc);
    border:1px solid var(--trait);
    border-radius:10px;
    padding:14px 16px;
    transition:border-color .15s ease, box-shadow .15s ease;
  }
  .fmorse textarea:focus{
    outline:none;
    border-color:var(--bleu-clair);
    box-shadow:0 0 0 3px rgba(41,138,212,.15);
  }
  .fmorse .fm-sortie-bloc{margin-top:22px}
  .fmorse .fm-sortie{
    font-family:"JetBrains Mono",ui-monospace,monospace;
    font-size:1.2rem;
    line-height:2;
    letter-spacing:.02em;
    color:var(--bleu);
    background:var(--fond-doux);
    border:1px solid var(--trait);
    border-radius:10px;
    padding:16px 18px;
    min-height:64px;
    word-break:break-word;
    white-space:pre-wrap;
  }
  .fmorse .fm-sortie:empty::before{
    content:"Le code Morse s’affichera ici.";
    color:var(--gris);
    font-family:"DM Sans",sans-serif;
    font-size:.95rem;
    letter-spacing:normal;
  }
  .fmorse .fm-car{
    display:inline-block;
    padding:0 2px;
    border-radius:4px;
    transition:background .08s linear, color .08s linear;
  }
  .fmorse .fm-car.actif{
    background:var(--bleu-clair);
    color:var(--blanc);
  }
  .fmorse .fm-controles{
    display:grid;
    grid-template-columns:1fr 1fr;
    gap:20px;
    margin-top:22px;
    padding:20px;
    background:var(--fond-doux);
    border:1px solid var(--trait);
    border-radius:10px;
  }
  .fmorse .fm-reglage .fm-reglage-tete{
    display:flex;
    justify-content:space-between;
    align-items:baseline;
    margin-bottom:8px;
  }
  .fmorse .fm-reglage-tete span:first-child{
    font-size:.82rem;
    font-weight:600;
    text-transform:uppercase;
    letter-spacing:.05em;
    color:var(--bleu);
  }
  .fmorse .fm-valeur{
    font-family:"JetBrains Mono",monospace;
    font-weight:700;
    font-size:.95rem;
    color:var(--encre);
  }
  .fmorse input[type=range]{
    width:100%;
    height:6px;
    -webkit-appearance:none;
    appearance:none;
    background:var(--trait);
    border-radius:3px;
    outline:none;
  }
  .fmorse input[type=range]::-webkit-slider-thumb{
    -webkit-appearance:none;
    appearance:none;
    width:20px;height:20px;border-radius:50%;
    background:var(--bleu);
    cursor:pointer;
    border:3px solid var(--blanc);
    box-shadow:0 0 0 1px var(--trait);
  }
  .fmorse input[type=range]::-moz-range-thumb{
    width:20px;height:20px;border-radius:50%;
    background:var(--bleu);
    cursor:pointer;
    border:3px solid var(--blanc);
  }
  .fmorse input[type=range]:focus-visible::-webkit-slider-thumb{
    box-shadow:0 0 0 3px rgba(41,138,212,.35);
  }
  .fmorse .fm-boutons{
    display:flex;
    flex-wrap:wrap;
    gap:12px;
    margin-top:22px;
  }
  .fmorse button{
    font-family:"DM Sans",sans-serif;
    font-size:1rem;
    font-weight:600;
    padding:12px 22px;
    border-radius:10px;
    cursor:pointer;
    border:1px solid transparent;
    transition:background .15s ease, opacity .15s ease;
  }
  .fmorse .fm-jouer{background:var(--bleu);color:var(--blanc)}
  .fmorse .fm-jouer:hover{background:var(--bleu-clair)}
  .fmorse .fm-stop{background:var(--blanc);color:var(--bleu);border-color:var(--trait)}
  .fmorse .fm-stop:hover{background:var(--fond-doux)}
  .fmorse .fm-copier{background:var(--blanc);color:var(--gris);border-color:var(--trait)}
  .fmorse .fm-copier:hover{background:var(--fond-doux)}
  .fmorse button:disabled{opacity:.45;cursor:not-allowed}
  .fmorse button:focus-visible{outline:2px solid var(--bleu-clair);outline-offset:2px}
  .fmorse .fm-note{
    margin-top:16px;
    font-size:.85rem;
    color:var(--gris);
  }
  @media (max-width:640px){
    .fmorse .fm-wrap{padding:20px}
    .fmorse .fm-controles{grid-template-columns:1fr}
    .fmorse .fm-boutons button{flex:1 1 auto}
  }
  @media (prefers-reduced-motion:reduce){
    .fmorse *{transition:none!important}
  }
</style>
</head>
<body>
<div class="fmorse">
  <div class="fm-wrap">
    <h2 class="fm-titre">Traducteur de code Morse</h2>
    <p class="fm-sous">Saisissez votre texte : la traduction s’affiche en direct, et vous pouvez l’écouter à la vitesse de votre choix.</p>

    <label for="fm-texte">Texte à traduire</label>
    <textarea id="fm-texte" placeholder="Entrez votre texte ici — lettres, chiffres et ponctuation sont pris en charge."></textarea>

    <div class="fm-sortie-bloc">
      <label for="fm-sortie">Code Morse</label>
      <div id="fm-sortie" class="fm-sortie" aria-live="polite"></div>
    </div>

    <div class="fm-controles">
      <div class="fm-reglage">
        <div class="fm-reglage-tete">
          <span>Vitesse</span>
          <span class="fm-valeur"><span id="fm-wpm-val">20</span> WPM</span>
        </div>
        <input type="range" id="fm-wpm" min="5" max="35" step="1" value="20" aria-label="Vitesse en mots par minute">
      </div>
      <div class="fm-reglage">
        <div class="fm-reglage-tete">
          <span>Tonalité</span>
          <span class="fm-valeur"><span id="fm-freq-val">600</span> Hz</span>
        </div>
        <input type="range" id="fm-freq" min="400" max="900" step="10" value="600" aria-label="Fréquence de la tonalité en hertz">
      </div>
    </div>

    <div class="fm-boutons">
      <button type="button" class="fm-jouer" id="fm-jouer">Écouter</button>
      <button type="button" class="fm-stop" id="fm-stop" disabled>Arrêter</button>
      <button type="button" class="fm-copier" id="fm-copier">Copier le Morse</button>
    </div>

    <p class="fm-note">Chronologie standard PARIS : point = 1 temps, trait = 3 temps, espace entre signes = 1 temps, entre lettres = 3 temps, entre mots = 7 temps. À 20 WPM, un temps dure 60 ms.</p>
  </div>
</div>

<script>
(function(){
  var 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':'--..',
    '0':'-----','1':'.----','2':'..---','3':'...--','4':'....-','5':'.....',
    '6':'-....','7':'--...','8':'---..','9':'----.',
    '.':'.-.-.-',',':'--..--','?':'..--..',"'":'.----.','!':'-.-.--',
    '/':'-..-.','(':'-.--.',')':'-.--.-','&':'.-...',':':'---...',
    ';':'-.-.-.','=':'-...-','+':'.-.-.','-':'-....-','_':'..--.-',
    '"':'.-..-.','$':'...-..-','@':'.--.-.',
    'É':'..-..','È':'.-..-','À':'.--.-','Ç':'-.-..','Ù':'..--'
  };

  var texte  = document.getElementById('fm-texte');
  var sortie = document.getElementById('fm-sortie');
  var wpm    = document.getElementById('fm-wpm');
  var freq   = document.getElementById('fm-freq');
  var wpmVal = document.getElementById('fm-wpm-val');
  var freqVal= document.getElementById('fm-freq-val');
  var bJouer = document.getElementById('fm-jouer');
  var bStop  = document.getElementById('fm-stop');
  var bCopier= document.getElementById('fm-copier');

  var audioCtx = null;
  var timers = [];
  var enLecture = false;
  var lettres = [];   // modele : { code, span, motAvant }

  // --- Traduction en direct + construction du modele ---
  function traduire(){
    var t = texte.value;
    sortie.innerHTML = '';
    lettres = [];
    var motAvant = false;     // un espace precede-t-il la prochaine lettre ?
    var premiere = true;

    for (var i=0;i<t.length;i++){
      var brut = t[i];
      if (/\s/.test(brut)){
        if (!premiere) motAvant = true;
        sortie.appendChild(document.createTextNode('   '));
        continue;
      }
      var code = MORSE[brut.toUpperCase()];
      if (!code) continue;    // caractere non pris en charge : ignore

      var span = document.createElement('span');
      span.className = 'fm-car';
      span.textContent = code;
      sortie.appendChild(span);
      sortie.appendChild(document.createTextNode(' '));

      lettres.push({ code: code, span: span, motAvant: motAvant });
      motAvant = false;
      premiere = false;
    }
  }

  function texteMorse(){
    return texte.value.toUpperCase().split('').map(function(c){
      if (/\s/.test(c)) return '/';
      return MORSE[c] || '';
    }).filter(function(x){return x!=='';}).join(' ');
  }

  // --- Lecture audio, timing PARIS ---
  function stop(){
    enLecture = false;
    timers.forEach(clearTimeout);
    timers = [];
    if (audioCtx){ try{ audioCtx.close(); }catch(e){} audioCtx = null; }
    var actifs = sortie.querySelectorAll('.fm-car.actif');
    for (var i=0;i<actifs.length;i++) actifs[i].classList.remove('actif');
    bStop.disabled = true;
    bJouer.disabled = false;
  }

  function jouer(){
    stop();
    if (!lettres.length) return;

    audioCtx = new (window.AudioContext || window.webkitAudioContext)();
    var u  = 1.2 / parseInt(wpm.value,10);   // duree d'un temps, en secondes
    var f  = parseInt(freq.value,10);
    var t0 = audioCtx.currentTime + 0.15;
    var curseur = t0;

    var osc  = audioCtx.createOscillator();
    var gain = audioCtx.createGain();
    osc.type = 'sine';
    osc.frequency.value = f;
    gain.gain.value = 0;
    osc.connect(gain).connect(audioCtx.destination);
    osc.start(t0);

    function bip(debut, duree){
      var r = 0.006; // rampe anti-clic
      gain.gain.setValueAtTime(0, debut);
      gain.gain.linearRampToValueAtTime(0.28, debut + r);
      gain.gain.setValueAtTime(0.28, debut + duree - r);
      gain.gain.linearRampToValueAtTime(0, debut + duree);
    }

    enLecture = true;
    bJouer.disabled = true;
    bStop.disabled = false;

    for (var i=0;i<lettres.length;i++){
      var L = lettres[i];

      // espace AVANT la lettre : 3 temps entre lettres, 7 temps entre mots
      if (i > 0) curseur += (L.motAvant ? 7*u : 3*u);

      // surlignage synchronise
      (function(span, debut){
        timers.push(setTimeout(function(){
          if(!enLecture) return;
          var a = sortie.querySelectorAll('.fm-car.actif');
          for (var k=0;k<a.length;k++) a[k].classList.remove('actif');
          span.classList.add('actif');
        }, (debut - t0) * 1000));
      })(L.span, curseur);

      // signes de la lettre, avec 1 temps entre chaque signe
      for (var j=0;j<L.code.length;j++){
        var d = L.code[j]==='-' ? 3*u : u;
        bip(curseur, d);
        curseur += d;
        if (j < L.code.length - 1) curseur += u;
      }
    }

    var finMs = (curseur - t0) * 1000 + 250;
    timers.push(setTimeout(function(){ if(enLecture) stop(); }, finMs));
  }

  // --- Evenements ---
  texte.addEventListener('input', function(){ if(enLecture) stop(); traduire(); });
  wpm.addEventListener('input',  function(){ wpmVal.textContent  = wpm.value;  });
  freq.addEventListener('input', function(){ freqVal.textContent = freq.value; });
  bJouer.addEventListener('click', jouer);
  bStop.addEventListener('click', stop);
  bCopier.addEventListener('click', function(){
    var m = texteMorse();
    if(!m) return;
    navigator.clipboard.writeText(m).then(function(){
      var ancien = bCopier.textContent;
      bCopier.textContent = 'Copié';
      setTimeout(function(){ bCopier.textContent = ancien; }, 1200);
    });
  });

  traduire();
})();
</script>
</body>
</html>