Resources · Breathwork

Box Breathing Guide

Follow the orb through four equal counts — in, hold, out, hold. Calm but alert, it steadies the nervous system and sharpens focus. Lovely before anything that needs a clear, steady mind.

4 · 4 · 4 · 4 Box Breathing
Round 1
Breathe
Find a comfortable position and relax your shoulders

A gentle wellbeing practice, not medical advice. If you feel light-headed, return to normal breathing.

(function(){ // ---- phase definitions (seconds) ---- const PHASES = [ { key:"inhale", label:"Breathe in", secs:4, color:"#D09C62", scale:1.9 }, { key:"hold1", label:"Hold", secs:4, color:"#BF7A5A", scale:1.9 }, { key:"exhale", label:"Breathe out",secs:4, color:"#3E8DA6", scale:1.0 }, { key:"hold2", label:"Hold", secs:4, color:"#4C434E", scale:1.0 }, ]; // ---- box geometry (matches SVG rect x=14 y=14 w=72 h=72) ---- const BX0=14, BY0=14, BX1=86, BY1=86, SIDE=72, PERIM=4*SIDE; // 288 // dot position along one edge for a given phase, param u in [0,1] function dotPos(idx, u){ switch(idx){ case 0: return [BX0 + u*SIDE, BY0]; // inhale: top, L->R case 1: return [BX1, BY0 + u*SIDE]; // hold1: right, T->B case 2: return [BX1 - u*SIDE, BY1]; // exhale: bottom, R->L case 3: return [BX0, BY1 - u*SIDE]; // hold2: left, B->T } } const phaseEl = document.getElementById('phase'); const countEl = document.getElementById('count'); const cue = document.getElementById('cue'); const roundsEl = document.getElementById('rounds'); const startBtn = document.getElementById('startBtn'); const cinemaBtn = document.getElementById('cinemaBtn'); const soundToggle = document.getElementById('soundToggle'); const boxDot = document.getElementById('boxDot'); const boxTrail = document.getElementById('boxTrail'); let running = false; let phaseIdx = 0; let phaseStart = 0; let round = 1; let rafId = null; let audioCtx = null, osc = null, masterGain = null, audioReady = false; // Build a persistent audio graph from inside a user gesture (reliable on mobile). function initAudio(){ if(audioReady) { if(audioCtx.state==='suspended') audioCtx.resume(); return; } try{ audioCtx = new (window.AudioContext||window.webkitAudioContext)(); masterGain = audioCtx.createGain(); masterGain.gain.value = 0.0001; osc = audioCtx.createOscillator(); osc.type = 'sine'; osc.frequency.value = 330; osc.connect(masterGain); masterGain.connect(audioCtx.destination); osc.start(); audioReady = true; if(audioCtx.state==='suspended') audioCtx.resume(); }catch(e){} } // gentle tone to mark phase changes — ramps the persistent gain up then down function playTone(freq, dur){ if(!soundToggle.checked || !audioReady) return; try{ if(audioCtx.state==='suspended') audioCtx.resume(); const t = audioCtx.currentTime; osc.frequency.setValueAtTime(freq, t); masterGain.gain.cancelScheduledValues(t); masterGain.gain.setValueAtTime(Math.max(masterGain.gain.value,0.0001), t); masterGain.gain.exponentialRampToValueAtTime(0.14, t+0.08); masterGain.gain.exponentialRampToValueAtTime(0.0001, t+Math.max(dur,0.3)); }catch(e){} } const cues = { inhale: "Draw the breath in slowly through your nose", hold1: "Hold gently — relaxed, not clenched", exhale: "Release steadily through your mouth", hold2: "Hold empty, soft and still", }; const phaseTone = { inhale:330, hold1:392, exhale:262, hold2:294 }; function applyPhase(p){ phaseEl.textContent = p.label; cue.textContent = cues[p.key]; // recolor the dot and trail to the phase colour boxDot.style.stroke = p.color; boxDot.style.filter = "drop-shadow(0 0 10px " + p.color + ") drop-shadow(0 0 4px #fff)"; boxTrail.style.stroke = p.color; // at the very start of a new cycle (inhale), reset the trail to empty if(phaseIdx === 0){ boxTrail.style.transition = "none"; boxTrail.style.strokeDashoffset = PERIM; void boxTrail.getBoundingClientRect(); boxTrail.style.transition = "stroke .6s ease"; } playTone(phaseTone[p.key], Math.min(p.secs,1.1)); } function tick(now){ if(!running) return; const p = PHASES[phaseIdx]; const elapsed = (now - phaseStart)/1000; const remain = Math.max(0, p.secs - elapsed); countEl.textContent = Math.ceil(remain); const frac = Math.min(1, elapsed / p.secs); // move the dot along the current edge const pos = dotPos(phaseIdx, frac); boxDot.setAttribute('cx', pos[0].toFixed(2)); boxDot.setAttribute('cy', pos[1].toFixed(2)); // fill the trail progressively (clockwise from top-left) const filled = (phaseIdx + frac) * SIDE; boxTrail.style.transition = "none"; boxTrail.style.strokeDashoffset = (PERIM - filled).toFixed(2); if(elapsed >= p.secs){ phaseIdx = (phaseIdx + 1) % PHASES.length; if(phaseIdx === 0){ round++; roundsEl.textContent = "Round " + round; } phaseStart = now; applyPhase(PHASES[phaseIdx]); } rafId = requestAnimationFrame(tick); } function start(){ initAudio(); if(running){ stop(); return; } running = true; startBtn.textContent = "Pause"; phaseIdx = 0; round = 1; roundsEl.textContent = "Round 1"; boxDot.setAttribute('cx', BX0); boxDot.setAttribute('cy', BY0); applyPhase(PHASES[0]); phaseStart = performance.now(); rafId = requestAnimationFrame(tick); } function stop(){ running = false; startBtn.textContent = "Resume"; cancelAnimationFrame(rafId); } startBtn.addEventListener('click', start); // unlock/prepare audio the moment the user enables Tones (a user gesture) soundToggle.addEventListener('change', ()=>{ if(soundToggle.checked) initAudio(); }); cinemaBtn.addEventListener('click', ()=>{ document.querySelector('.breath-section').classList.toggle('cinema'); cinemaBtn.textContent = document.querySelector('.breath-section').classList.contains('cinema') ? "Exit cinema" : "Cinema mode"; }); })();