{"id":1595,"date":"2025-09-08T17:38:24","date_gmt":"2025-09-08T15:38:24","guid":{"rendered":"https:\/\/centrazensudgrenoble.com\/?page_id=1595"},"modified":"2025-10-01T18:24:37","modified_gmt":"2025-10-01T16:24:37","slug":"respiration-carree","status":"publish","type":"page","link":"https:\/\/centrazensudgrenoble.com\/index.php\/respiration-carree\/","title":{"rendered":"Respiration carr\u00e9e"},"content":{"rendered":"\n<!doctype html>\n<html lang=\"fr\">\n<head>\n  <meta charset=\"utf-8\" \/>\n  <meta name=\"viewport\" content=\"width=device-width,initial-scale=1\" \/>\n  <title>Respiration carr\u00e9e \u2014 Animation<\/title>\n  <style>\n    :root{\n      --bg:#eaf6f9;\n      --accent:#3b82f6;\n      --muted:#6b7280;\n      --size:320px;\n      --side-duration:4000; \/* ms par c\u00f4t\u00e9 *\/\n    }\n    html,body{height:100%;margin:0;font-family:Inter, system-ui, -apple-system, Roboto, 'Helvetica Neue', Arial;color:#0f172a;overflow-x:hidden;}\n    body{display:flex;align-items:center;justify-content:center;background:linear-gradient(180deg,var(--bg),#ffffff);min-height:100vh;padding:20px 10px;}\n    .card{width:calc(var(--size) + 40px);padding:22px;border-radius:18px;background:rgba(255,255,255,0.8);box-shadow:0 8px 30px rgba(15,23,42,0.08);text-align:center;max-width:100%;box-sizing:border-box;}\n    h1{font-size:18px;margin:0 0 10px}\n    p{margin:0 0 18px;color:var(--muted);font-size:14px}\n\n    .stage{\n  display: grid;\n  grid-template-columns: 1fr 200px; \/* colonne de droite plus large *\/\n  gap: 140px; \/* plus d'espace entre les deux colonnes *\/\n  align-items: flex-start;\n  margin-top: 18px;\n}\n\n    \/* Responsive pour mobile *\/\n    @media (max-width: 768px) {\n      body {\n        padding: 15px 10px;\n        align-items: flex-start;\n      }\n      \n      .card {\n        width: 100%;\n        max-width: none;\n        padding: 20px 15px;\n        margin-top: 20px;\n      }\n      \n      .stage {\n        grid-template-columns: 1fr;\n        gap: 25px;\n        text-align: center;\n      }\n      \n      .visu {\n        width: min(220px, calc(100vw - 80px));\n        height: min(220px, calc(100vw - 80px));\n        margin: 0 auto;\n      }\n      \n      .controls {\n        flex-wrap: wrap;\n        gap: 8px;\n      }\n      \n      button {\n        min-width: 90px;\n      }\n      \n      .breath-dot {\n        width: min(200px, calc(100vw - 80px));\n        height: 60px;\n        margin: 0 auto;\n      }\n    }\n    .visu{width:var(--size);height:var(--size);display:flex;align-items:center;justify-content:center}\n\n    svg{width:100%;height:100%}\n    .square-line{stroke:var(--accent);stroke-width:4;stroke-linecap:round;fill:none;stroke-dasharray:1000;stroke-dashoffset:1000;transition:stroke-dashoffset calc(var(--side-duration) * 1ms) linear}\n    \n    @media (max-width: 768px) {\n      .square-line {\n        stroke-width: 5;\n      }\n    }\n\n    .label{font-size:18px;font-weight:600;color:var(--accent);min-height:28px}\n    .hint{font-size:13px;color:var(--muted)}\n\n    .controls{\n  display:flex;\n  gap:10px;\n  justify-content:center;\n  margin-top:24px;\n}\nbutton{\n  background:var(--accent);\n  color:white;\n  border:none;\n  padding:9px 18px;\n  border-radius:12px;\n  cursor:pointer;\n  font-weight:600;\n  font-size:15px;\n  box-shadow:0 2px 8px rgba(59,130,246,0.07);\n  transition:background 0.18s, box-shadow 0.18s, color 0.18s;\n}\nbutton.secondary{\n  background:transparent;\n  color:var(--accent);\n  border:1.5px solid #dbeafe;\n}\nbutton:disabled{\n  opacity:0.6;\n  cursor:not-allowed;\n}\nbutton:not(:disabled):hover{\n  background:#2563eb;\n  color:white;\n  box-shadow:0 4px 16px rgba(59,130,246,0.13);\n}\ninput[type=\"number\"]{\n  border:1.5px solid #dbeafe;\n  border-radius:8px;\n  padding:7px 10px;\n  font-size:15px;\n  width:54px;\n  background:#f8fafc;\n  color:#2563eb;\n  font-weight:600;\n  outline:none;\n  box-shadow:0 1px 4px rgba(59,130,246,0.04);\n  transition:border 0.18s, box-shadow 0.18s;\n  margin:0 2px;\n}\ninput[type=\"number\"]:focus{\n  border:1.5px solid var(--accent);\n  box-shadow:0 0 0 2px #3b82f633;\n}\n.hint input[type=\"number\"]{\n  width:54px;\n  min-width:0;\n  text-align:center;\n}\n\n    \/* cercle qui gonfle\/retrecit pour l'inspire\/expire *\/\n    .breath-dot{width:200px;height:60px;border-radius:999px;background:linear-gradient(135deg,rgba(59,130,246,0.15),rgba(59,130,246,0.06));display:flex;align-items:center;justify-content:center}\n    .dot{width:18px;height:18px;border-radius:999px;background:var(--accent);transition:transform calc(var(--side-duration) * 1ms) ease-in-out}\n\n    footer{font-size:12px;color:var(--muted);margin-top:12px}\n  <\/style>\n<\/head>\n<body>\n  <div class=\"card\">\n    <h1>Respiration carr\u00e9e<\/h1>\n    <p>Suivez le carr\u00e9 : inspirez \u2014 retenez \u2014 expirez \u2014<br>\n    retenez vide. Chaque c\u00f4t\u00e9 = 4 secondes (modifiable).<\/p>\n\n    <div class=\"stage\">\n      <div class=\"visu\">\n        <svg viewBox=\"0 0 220 220\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\">\n          <!-- quatre lignes formant un carr\u00e9 ; longueurs approxim\u00e9es -->\n          <line id=\"side1\" class=\"square-line\" x1=\"40\" y1=\"40\" x2=\"180\" y2=\"40\" \/>\n          <line id=\"side2\" class=\"square-line\" x1=\"180\" y1=\"40\" x2=\"180\" y2=\"180\" \/>\n          <line id=\"side3\" class=\"square-line\" x1=\"180\" y1=\"180\" x2=\"40\" y2=\"180\" \/>\n          <line id=\"side4\" class=\"square-line\" x1=\"40\" y1=\"180\" x2=\"40\" y2=\"40\" \/>\n          <!-- cercle central l\u00e9g\u00e8rement transparent -->\n          <circle cx=\"110\" cy=\"110\" r=\"34\" fill=\"rgba(59,130,246,0.12)\" stroke=\"rgba(59,130,246,0.2)\" stroke-width=\"2\" \/>\n        <\/svg>\n      <\/div>\n\n      <div>\n        <div class=\"label\" id=\"phaseLabel\">Pr\u00eat<\/div>\n        <div class=\"hint\" id=\"countdown\">Dur\u00e9e par phase : 4s<\/div>\n        <div class=\"hint\" style=\"margin-top:14px; display:flex; align-items:center; gap:10px; justify-content:center;\">\n  <span>Dur\u00e9e d&#8217;une phase :<\/span>\n  <input id=\"phaseDuration\" type=\"number\" min=\"1\" max=\"30\" value=\"4\">\n  <span>s<\/span>\n<\/div>\n\n        <div style=\"height:18px\"><\/div>\n        <div class=\"breath-dot\" aria-hidden=\"true\">\n          <div class=\"dot\" id=\"dot\"><\/div>\n        <\/div>\n\n        <div class=\"controls\">\n          <button id=\"start\">D\u00e9marrer<\/button>\n          <button id=\"pause\" class=\"secondary\" disabled>Pause<\/button>\n          <button id=\"reset\" class=\"secondary\">R\u00e9initialiser<\/button>\n        <\/div>\n\n        <div style=\"height:8px\"><\/div>\n        <div class=\"hint\" style=\"margin-top:10px; display:flex; align-items:center; gap:10px; justify-content:center;\">\n  <span>Cycles :<\/span>\n  <input id=\"loops\" type=\"number\" min=\"1\" value=\"4\">\n<\/div>\n      <\/div>\n    <\/div>\n\n    <footer><\/footer>\n  <\/div>\n\n  <script>\n    \/\/ param\u00e8tres\n    const phaseDurationInput = document.getElementById('phaseDuration');\nlet sideDuration = parseInt(phaseDurationInput.value, 10) * 1000;\n    const sides = [document.getElementById('side1'),document.getElementById('side2'),document.getElementById('side3'),document.getElementById('side4')];\n    const label = document.getElementById('phaseLabel');\n    const countdown = document.getElementById('countdown');\n    const dot = document.getElementById('dot');\n    const startBtn = document.getElementById('start');\n    const pauseBtn = document.getElementById('pause');\n    const resetBtn = document.getElementById('reset');\n    const loopsInput = document.getElementById('loops');\n\n    \/\/ initialiser les traits (dashoffset = longueur) \u2014 on force une valeur grande pour cacher\n    sides.forEach((el)=>{ el.style.strokeDasharray = 140; el.style.strokeDashoffset = 140; });\n\n    let running = false;\n    let paused = false;\n    let currentPhase = 0; \/\/ 0..3\n    let cycle = 0;\n    let targetCycles = parseInt(loopsInput.value,10) || 1;\n    let timer = null;\n    let phaseTimeout = null;\n    let remaining = 0; \/\/ pour pause\n\n    const PHASE_NAMES = ['Inspire','Retiens','Expire','Retiens vide'];\n\n    function updateCountdown(msLeft){\n      countdown.textContent = `Temps restant dans la phase : ${Math.ceil(msLeft\/1000)}s`;\n    }\n\n    function setDotScale(phase){\n      \/\/ phase 0 = inspire (gonfle), 2 = expire (d\u00e9gonfle)\n      if(phase === 0) dot.style.transform = 'scale(2.4)';\n      else if(phase === 2) dot.style.transform = 'scale(0.5)';\n      else dot.style.transform = 'scale(1)';\n    }\n\n    function runPhase(index, onDone){\n      currentPhase = index;\n      label.textContent = PHASE_NAMES[index];\n      setDotScale(index);\n\n      \/\/ dur\u00e9e personnalis\u00e9e\n      const duration = sideDuration;\n\n      \/\/ dessiner le c\u00f4t\u00e9 correspondant\n      const el = sides[index];\n      el.style.transition = `stroke-dashoffset ${duration}ms linear`;\n      void el.offsetWidth;\n      el.style.strokeDashoffset = '0';\n\n      const start = performance.now();\n      function tick(now){\n        if(!running) return;\n        const elapsed = now - start;\n        const left = Math.max(0, duration - elapsed);\n        updateCountdown(left);\n        if(elapsed >= duration){\n          onDone();\n        } else {\n          phaseTimeout = requestAnimationFrame(tick);\n        }\n      }\n      phaseTimeout = requestAnimationFrame(tick);\n    }\n\n    function resetSides(){\n      sides.forEach((el)=>{ el.style.transition = 'none'; el.style.strokeDashoffset = 140; });\n      \/\/ force reflow\n      void document.body.offsetWidth;\n    }\n\nfunction startSession(){\n  if(running) return;\n  running = true; paused = false;\n  startBtn.disabled = true; pauseBtn.disabled = false;\n  targetCycles = Math.max(1, parseInt(loopsInput.value,10) || 1);\n  cycle = 0;\n  currentPhase = 0;\n  resetSides();\n\n  \/\/ \u26a1 attendre un petit peu avant de commencer\n  setTimeout(()=> proceedPhase(0), 50);\n}\n\n    function proceedPhase(phaseIndex){\n      if(!running) return;\n\n      \/\/ if starting a new cycle and phase 0, increment cycle count\n      if(phaseIndex === 0 && currentPhase === 0){\n        \/\/ nothing here\n      }\n\n      runPhase(phaseIndex, ()=>{\n        \/\/ after finished drawing side\n        \/\/ reset the drawn side to be visible (keep it drawn)\n        sides[phaseIndex].style.transition = 'none';\n        sides[phaseIndex].style.strokeDashoffset = '0';\n\n        const next = (phaseIndex + 1) % 4;\n        if(next === 0){\n          cycle += 1;\n        }\n\n        if(cycle >= targetCycles && next === 0){\n          \/\/ finished all cycles\n          finishSession();\n          return;\n        }\n\n        \/\/ short tiny pause between phases to allow transition reset (20ms)\n        setTimeout(()=>{\n          \/\/ if we wrapped to next cycle and want to reset drawing, do so\n          if(next === 0){\n            \/\/ fade out puis reset\n            sides.forEach(s=>s.style.opacity = '0.4');\n            setTimeout(()=>{\n              resetSides();\n              sides.forEach(s=>s.style.opacity='1');\n              \/\/ attendre un peu avant de lancer la phase suivante\n              setTimeout(()=>proceedPhase(next), 60);\n            }, 300);\n          } else {\n            proceedPhase(next);\n          }\n        }, 60);\n      });\n    }\n\n    function finishSession(){\n      running = false; startBtn.disabled = false; pauseBtn.disabled = true;\n      label.textContent = 'Termin\u00e9 \u2014 Bien jou\u00e9';\n      countdown.textContent = `Cycles compl\u00e9t\u00e9s : ${cycle}`;\n      \/\/ r\u00e9initialiser visuel\n      setDotScale(-1);\n    }\n\n    function pauseSession(){\n      if(!running) return;\n      paused = true; running = false; startBtn.disabled = false; pauseBtn.disabled = true;\n      label.textContent = 'En pause';\n      if(phaseTimeout) cancelAnimationFrame(phaseTimeout);\n      \/\/ NOTE: this simple implementation does not capture partial progress of current side's transition.\n      \/\/ For a precise pause\/resume you'd capture elapsed time and animate the remaining length. Simplicity kept here.\n    }\n\n    function resetSession(){\n      if(phaseTimeout) cancelAnimationFrame(phaseTimeout);\n      running = false; paused = false; startBtn.disabled = false; pauseBtn.disabled = true;\n      label.textContent = 'Pr\u00eat';\n      countdown.textContent = `Dur\u00e9e par phase : ${sideDuration\/1000}s`;\n      resetSides();\n      setDotScale(-1); \/\/ <-- remet la bulle \u00e0 la taille normale\n    }\n\n    startBtn.addEventListener('click', startSession);\n    pauseBtn.addEventListener('click', pauseSession);\n    resetBtn.addEventListener('click', resetSession);\n\n    \/\/ accessibility : enter key on start\n    loopsInput.addEventListener('change', ()=>{ const v = parseInt(loopsInput.value,10); if(isNaN(v) || v < 1) loopsInput.value = '1'; });\n\n    \/\/ afficher dur\u00e9e dans l'UI\n    document.getElementById('countdown').textContent = `Dur\u00e9e par phase : ${sideDuration\/1000}s`;\n\nphaseDurationInput.addEventListener('input', () => {\n  let v = Math.max(1, parseInt(phaseDurationInput.value, 10) || 4);\n  phaseDurationInput.value = v;\n  sideDuration = v * 1000;\n  countdown.textContent = `Dur\u00e9e par phase : ${v}s`;\n});\n  <\/script>\n<\/body>\n<\/html>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Respiration carr\u00e9e \u2014 Animation Respiration carr\u00e9e Suivez le carr\u00e9 : inspirez \u2014 retenez \u2014 expirez \u2014 retenez vide. Chaque c\u00f4t\u00e9 = 4 secondes (modifiable). Pr\u00eat Dur\u00e9e par phase : 4s Dur\u00e9e d&#8217;une phase : s D\u00e9marrer Pause R\u00e9initialiser Cycles :<\/p>\n","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"_uag_custom_page_level_css":"","_swt_meta_header_display":false,"_swt_meta_footer_display":false,"_swt_meta_site_title_display":false,"_swt_meta_sticky_header":false,"_swt_meta_transparent_header":false,"footnotes":""},"class_list":["post-1595","page","type-page","status-publish","hentry"],"uagb_featured_image_src":{"full":false,"thumbnail":false,"medium":false,"medium_large":false,"large":false,"1536x1536":false,"2048x2048":false,"woocommerce_thumbnail":false,"woocommerce_single":false,"woocommerce_gallery_thumbnail":false},"uagb_author_info":{"display_name":"admin9646","author_link":"https:\/\/centrazensudgrenoble.com\/index.php\/author\/admin9646\/"},"uagb_comment_info":0,"uagb_excerpt":"Respiration carr\u00e9e \u2014 Animation Respiration carr\u00e9e Suivez le carr\u00e9 : inspirez \u2014 retenez \u2014 expirez \u2014 retenez vide. Chaque c\u00f4t\u00e9 = 4 secondes (modifiable). Pr\u00eat Dur\u00e9e par phase : 4s Dur\u00e9e d&#8217;une phase : s D\u00e9marrer Pause R\u00e9initialiser Cycles :","_links":{"self":[{"href":"https:\/\/centrazensudgrenoble.com\/index.php\/wp-json\/wp\/v2\/pages\/1595","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/centrazensudgrenoble.com\/index.php\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/centrazensudgrenoble.com\/index.php\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/centrazensudgrenoble.com\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/centrazensudgrenoble.com\/index.php\/wp-json\/wp\/v2\/comments?post=1595"}],"version-history":[{"count":31,"href":"https:\/\/centrazensudgrenoble.com\/index.php\/wp-json\/wp\/v2\/pages\/1595\/revisions"}],"predecessor-version":[{"id":1713,"href":"https:\/\/centrazensudgrenoble.com\/index.php\/wp-json\/wp\/v2\/pages\/1595\/revisions\/1713"}],"wp:attachment":[{"href":"https:\/\/centrazensudgrenoble.com\/index.php\/wp-json\/wp\/v2\/media?parent=1595"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}