/* Quantum UI sound design — hover hat on every menu word. Web Audio for low-latency retrigger. Volume already -30% baked in the file. */ (function () { var BASE = (location.pathname.indexOf('/it/') > -1 ? '../assets/' : 'assets/'); var ctx = null; var sounds = { menu: { src: BASE + 'hover-hat.mp3?a=2', buffer: null, loading: false }, section: { src: BASE + 'hover-hat2.mp3?a=2', buffer: null, loading: false }, cta: { src: BASE + 'hover-clap.mp3?a=2', buffer: null, loading: false }, wood: { src: BASE + 'hover-wood.mp3?a=2', buffer: null, loading: false }, xylo: { src: BASE + 'hover-xylo.mp3?a=2', buffer: null, loading: false }, tunnel: { src: BASE + 'hover-tunnel.mp3?a=2', buffer: null, loading: false }, wavepad: { src: BASE + 'wave-pad.mp3?a=4', buffer: null, loading: false }, cartadd: { src: BASE + 'cart-add.mp3', buffer: null, loading: false }, cartopen:{ src: BASE + 'cart-open.mp3', buffer: null, loading: false }, healthpad:{ src: BASE + 'health-pad.mp3', buffer: null, loading: false }, oraclepad:{ src: BASE + 'oracle-pad.mp3', buffer: null, loading: false }, infopad: { src: BASE + 'info-pad.mp3', buffer: null, loading: false }, homepad: { src: BASE + 'home-pad.mp3', buffer: null, loading: false }, apppad: { src: BASE + 'app-pad.mp3', buffer: null, loading: false }, pluginspad:{ src: BASE + 'plugins-pad.mp3', buffer: null, loading: false }, threedpad:{ src: BASE + 'threed-pad.mp3?a=3', buffer: null, loading: false }, contactspad:{ src: BASE + 'contacts-pad.mp3', buffer: null, loading: false }, shoppad:{ src: BASE + 'shop-pad.mp3', buffer: null, loading: false }, blackrosepad:{ src: BASE + 'blackrose-pad.mp3', buffer: null, loading: false }, justthispad:{ src: BASE + 'justthis-amb.mp3?b=2', buffer: null, loading: false } }; var master = null; var userPaused = false; function getVol() { var v = parseFloat(localStorage.getItem('qvolume')); return (isNaN(v) || v < 0 || v > 1) ? 0.8 : v; } function ensureCtx() { if (!ctx) { var AC = window.AudioContext || window.webkitAudioContext; if (!AC) return null; ctx = new AC(); master = ctx.createGain(); master.gain.value = getVol(); // persisted volume (default 0.8 = global -20%) master.connect(ctx.destination); ctx.onstatechange = function () { if (ctx.state === 'running') { Object.keys(sounds).forEach(function (k) { if (sounds[k].pending) { sounds[k].pending = false; play(k); } }); } }; } if (ctx.state === 'suspended' && !userPaused) ctx.resume(); return ctx; } // Floating audio control (bottom-right, every page): volume slider + play/pause function createAudioToggle() { if (document.getElementById('q-audio-ctrl')) return; var wrap = document.createElement('div'); wrap.id = 'q-audio-ctrl'; wrap.style.cssText = 'position:fixed;bottom:20px;right:20px;z-index:9998;display:flex;align-items:center;gap:11px'; var vol = document.createElement('input'); vol.type = 'range'; vol.min = '0'; vol.max = '1'; vol.step = '0.01'; vol.value = String(getVol()); vol.id = 'q-audio-vol'; vol.setAttribute('aria-label', 'Volume'); vol.style.cssText = 'width:96px;height:5px;accent-color:#a78bfa;cursor:pointer;opacity:.55;transition:opacity .25s ease;filter:drop-shadow(0 6px 14px rgba(0,0,0,.5))'; vol.addEventListener('mouseenter', function () { vol.style.opacity = '1'; }); vol.addEventListener('mouseleave', function () { vol.style.opacity = '.55'; }); vol.addEventListener('input', function () { var v = parseFloat(vol.value); localStorage.setItem('qvolume', v); // persist across pages ensureCtx(); if (master) master.gain.value = v; }); var btn = document.createElement('button'); btn.id = 'q-audio-toggle'; btn.setAttribute('aria-label', 'Play / pause sound'); btn.style.cssText = 'width:48px;height:48px;border-radius:50%;border:1px solid rgba(167,139,250,.4);background:rgba(10,15,44,.5);backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);color:#e7f3ff;opacity:.55;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:opacity .25s ease,transform .25s ease;box-shadow:0 10px 30px -12px rgba(0,0,0,.6)'; btn.addEventListener('mouseenter', function () { btn.style.opacity = '1'; btn.style.transform = 'scale(1.06)'; }); btn.addEventListener('mouseleave', function () { btn.style.opacity = '.55'; btn.style.transform = 'scale(1)'; }); var PAUSE = ''; var PLAY = ''; function icon() { btn.innerHTML = userPaused ? PLAY : PAUSE; } icon(); btn.addEventListener('click', function () { ensureCtx(); if (userPaused) { userPaused = false; if (ctx) ctx.resume(); } else { userPaused = true; if (ctx) ctx.suspend(); } icon(); }); wrap.appendChild(vol); wrap.appendChild(btn); document.body.appendChild(wrap); } function load(key) { var snd = sounds[key]; if (snd.buffer || snd.loading) return; var c = ensureCtx(); if (!c) return; snd.loading = true; fetch(snd.src).then(function (r) { return r.arrayBuffer(); }) .then(function (ab) { return c.decodeAudioData(ab); }) .then(function (buf) { snd.buffer = buf; if (snd.pending && c.state === 'running') { snd.pending = false; play(key); } }) .catch(function () { snd.loading = false; }); } function play(key, vol) { var c = ensureCtx(); var snd = sounds[key]; if (!c) return; if (!snd.buffer) { snd.pending = true; load(key); return; } if (c.state !== 'running') { snd.pending = true; c.resume(); return; } var s = c.createBufferSource(); s.buffer = snd.buffer; if (key === 'tunnel') { // Sync to the tunnel: if audio unlocks late, enter at the elapsed offset // so the stab never plays past the tunnel, and fade out on its tail. var g = c.createGain(); s.connect(g); g.connect(master); var dur = snd.buffer.duration; var elapsed = window.__introT0 ? Math.max(0, (performance.now() - window.__introT0) / 1000) : 0; var offset = Math.min(elapsed, Math.max(0, dur - 0.15)); var remaining = dur - offset; var fade = Math.min(1.2, remaining * 0.35); var now = c.currentTime; g.gain.setValueAtTime(1, now); g.gain.setValueAtTime(1, now + remaining - fade); g.gain.linearRampToValueAtTime(0.0001, now + remaining); s.start(0, offset); return; } if (key === 'wavepad' || key === 'healthpad' || key === 'oraclepad' || key === 'infopad' || key === 'homepad' || key === 'apppad' || key === 'pluginspad' || key === 'threedpad' || key === 'contactspad' || key === 'shoppad' || key === 'blackrosepad' || key === 'justthispad') { s.loop = true; } if (key === 'wavepad') { // long smooth fade-in for the Quantum Wave DAW background var fg = c.createGain(); var now = c.currentTime; fg.gain.setValueAtTime(0.0001, now); fg.gain.exponentialRampToValueAtTime(1, now + 5); s.connect(fg); fg.connect(master); s.start(0); return; } var v = (vol != null) ? vol : (key === 'menu' ? 0.64 : 1); if (v !== 1) { var mg = c.createGain(); mg.gain.value = v; s.connect(mg); mg.connect(master); } else { s.connect(master); } s.start(0); } // Intro tunnel: eager-decode immediately and fire the instant audio is allowed, // so there is no decode latency when the tunnel starts. var tunnelFired = false; function fireTunnel() { if (tunnelFired) return; var introEl = document.getElementById('intro'); if (!introEl || getComputedStyle(introEl).display === 'none' || introEl.classList.contains('hidden')) return; tunnelFired = true; play('tunnel'); // pending -> plays the moment the AudioContext is running } // Quantum Wave DAW page: pad starts immediately on open (pre-decoded, no latency). var IS_WAVE = location.pathname.indexOf('wave.html') > -1; var wavePadFired = false; function fireWavePad() { if (wavePadFired || !IS_WAVE) return; wavePadFired = true; play('wavepad'); } // Cart page (reached by clicking the menu cart icon) -> cash sound on open var IS_CART = location.pathname.indexOf('cart.html') > -1; var cartOpenFired = false; function fireCartOpen() { if (cartOpenFired || !IS_CART) return; cartOpenFired = true; play('cartopen'); } // Health page: ambient track starts on open (looped) var IS_HEALTH = location.pathname.indexOf('health.html') > -1; var healthFired = false; function fireHealth() { if (healthFired || !IS_HEALTH) return; healthFired = true; play('healthpad'); } // Oracle page: ambient track starts on open (looped) var IS_ORACLE = location.pathname.indexOf('oracle.html') > -1; var oracleFired = false; function fireOracle() { if (oracleFired || !IS_ORACLE) return; oracleFired = true; play('oraclepad'); } // App page: track starts on open (looped) var IS_APP = location.pathname.indexOf('app.html') > -1; var appFired = false; function fireApp() { if (appFired || !IS_APP) return; appFired = true; play('apppad'); } // Plugins page: track starts on open (looped) var IS_PLUGINS = location.pathname.indexOf('plugins.html') > -1; var pluginsFired = false; function firePlugins() { if (pluginsFired || !IS_PLUGINS) return; pluginsFired = true; play('pluginspad'); } // 3D page: track starts on open (looped) var IS_3D = location.pathname.indexOf('3d.html') > -1; var threedFired = false; function fire3D() { if (threedFired || !IS_3D) return; threedFired = true; play('threedpad'); } // Shop page: track starts on open (looped) var IS_SHOP = location.pathname.indexOf('shop.html') > -1; var shopFired = false; function fireShop() { if (shopFired || !IS_SHOP) return; shopFired = true; play('shoppad'); } // Just This product page: ambient forest loop on open var IS_JUSTTHIS = location.pathname.indexOf('product.html') > -1 && (new URLSearchParams(location.search).get('id') === 'just-this'); var justthisFired = false; function fireJustThis() { if (justthisFired || !IS_JUSTTHIS) return; justthisFired = true; play('justthispad'); } // Black Rose product page: track starts on open (looped) var IS_BLACKROSE = location.pathname.indexOf('black-rose.html') > -1; var blackroseFired = false; function fireBlackRose() { if (blackroseFired || !IS_BLACKROSE) return; blackroseFired = true; play('blackrosepad'); } // Contacts page: track starts on open (looped) var IS_CONTACTS = location.pathname.indexOf('contacts.html') > -1; var contactsFired = false; function fireContacts() { if (contactsFired || !IS_CONTACTS) return; contactsFired = true; play('contactspad'); } // Info page: ambient track starts on open (looped) var IS_INFO = location.pathname.indexOf('info.html') > -1; var infoFired = false; function fireInfo() { if (infoFired || !IS_INFO) return; infoFired = true; play('infopad'); } // Home page: background track (looped) — starts right after the tunnel intro ends. var pn = location.pathname; var IS_HOME = pn === '/' || pn === '/it/' || pn.indexOf('index.html') > -1; var homeFired = false; function introGone() { var i = document.getElementById('intro'); return !i || getComputedStyle(i).display === 'none' || i.classList.contains('hidden'); } function fireHome() { if (homeFired || !IS_HOME) return; homeFired = true; play('homepad'); } function setupHome() { if (!IS_HOME) return; load('homepad'); if (introGone()) { fireHome(); return; } var iv = setInterval(function () { if (introGone()) { clearInterval(iv); fireHome(); } }, 200); setTimeout(function () { clearInterval(iv); fireHome(); }, 9000); // safety } // iOS/Safari hard-unlock: play a 1-sample silent buffer INSIDE the gesture so the // AudioContext fully unlocks and page loops can play on touch (no mouse needed). function silentUnlock() { var c = ensureCtx(); if (!c || userPaused) return; try { var b = c.createBuffer(1, 1, 22050); var src = c.createBufferSource(); src.buffer = b; src.connect(c.destination); src.start(0); } catch (e) {} } // Unlock audio as early/broadly as possible (browsers gate audio behind a user gesture) function unlock() { ensureCtx(); silentUnlock(); load('menu'); load('section'); load('cta'); load('wood'); load('xylo'); load('tunnel'); fireTunnel(); load('wavepad'); fireWavePad(); load('cartopen'); fireCartOpen(); load('healthpad'); fireHealth(); load('oraclepad'); fireOracle(); load('infopad'); fireInfo(); load('homepad'); fireHome(); load('apppad'); fireApp(); load('pluginspad'); firePlugins(); load('threedpad'); fire3D(); load('contactspad'); fireContacts(); load('shoppad'); fireShop(); load('blackrosepad'); fireBlackRose(); load('justthispad'); fireJustThis(); } ['pointerdown', 'pointerup', 'mousedown', 'touchstart', 'touchend', 'click', 'keydown', 'wheel', 'scroll', 'mousemove'].forEach(function (ev) { window.addEventListener(ev, unlock, { once: true, passive: true }); }); // keep re-arming the unlock on touch until audio is actually running (mobile resilience) document.addEventListener('touchend', function rearm() { if (ctx && ctx.state === 'running') { document.removeEventListener('touchend', rearm); return; } unlock(); }, { passive: true }); function attach() { var seen = new Set(); createAudioToggle(); // Intro tunnel sound (buffer already eager-decoded at init -> no latency) fireTunnel(); // Pre-warm the HTTP cache for page-open sounds when hovering their menu link, // so the audio is already downloaded before the click -> no latency on arrival. var prefetched = {}; function prefetchFor(href) { var file = href.indexOf('health.html') > -1 ? 'health-pad.mp3' : href.indexOf('oracle.html') > -1 ? 'oracle-pad.mp3' : href.indexOf('info.html') > -1 ? 'info-pad.mp3' : href.indexOf('app.html') > -1 ? 'app-pad.mp3' : href.indexOf('plugins.html') > -1 ? 'plugins-pad.mp3' : href.indexOf('3d.html') > -1 ? 'threed-pad.mp3?a=3' : href.indexOf('contacts.html') > -1 ? 'contacts-pad.mp3' : href.indexOf('shop.html') > -1 ? 'shop-pad.mp3' : href.indexOf('wave.html') > -1 ? 'wave-pad.mp3?a=4' : href.indexOf('cart.html') > -1 ? 'cart-open.mp3' : null; if (!file || prefetched[file]) return; prefetched[file] = 1; try { fetch(BASE + file, { cache: 'force-cache' }); } catch (e) {} } // every menu word: nav links (skip language flags) document.querySelectorAll('nav a, .nav-main a, header nav a').forEach(function (a) { if (seen.has(a) || a.classList.contains('flag')) return; seen.add(a); a.addEventListener('mouseenter', function () { load('menu'); play('menu'); prefetchFor(a.getAttribute('href') || ''); }); }); // every single item inside Practice, Selected Work, How we work, Quantum Editions document.querySelectorAll('#oracle .service, #work .work-card, #process .step, #editions .teaser').forEach(function (el) { if (seen.has(el)) return; seen.add(el); el.addEventListener('mouseenter', function () { load('section'); play('section'); }); }); // App page cards (phone mockups) -> hi-hat (RK_DM6_Hihat_04 = section sound) document.querySelectorAll('.sos-frame').forEach(function (el) { if (seen.has(el)) return; seen.add(el); el.addEventListener('mouseenter', function () { load('section'); play('section'); }); }); // Cart page opened (via the menu cart icon) -> cash sound fireCartOpen(); // Health page -> ambient loop fireHealth(); // Oracle page -> ambient loop fireOracle(); // Info page -> ambient loop fireInfo(); // Home page -> background loop after the tunnel setupHome(); // App page -> looped track fireApp(); // Plugins page -> looped track firePlugins(); // 3D page -> looped track fire3D(); // Contacts page -> looped track fireContacts(); // Shop page -> looped track fireShop(); // Black Rose page -> looped track fireBlackRose(); // Just This product page -> ambient loop fireJustThis(); // Shop: hover over product cards -> same sound as Plugins cards (xylophone) document.querySelectorAll('.shop-card').forEach(function (el) { if (seen.has(el)) return; seen.add(el); el.addEventListener('mouseenter', function () { load('xylo'); play('xylo', 0.64); }); }); // Shop: click on "Add to cart" / "Subscribe" buttons document.querySelectorAll('.shop-card-cta').forEach(function (el) { if (seen.has(el)) return; seen.add(el); el.addEventListener('click', function () { load('cartadd'); play('cartadd'); }); }); // contact CTA: "Have a project that needs thinking?" + the email below it document.querySelectorAll('#contact h3, #contact .mail').forEach(function (el) { if (seen.has(el)) return; seen.add(el); el.addEventListener('mouseenter', function () { load('cta'); play('cta'); }); }); // Info page: xylophone (same as Plugins cards) on every section + every review. if (location.pathname.indexOf('info.html') > -1) { document.querySelectorAll('.m-box, .rv-form, .partners').forEach(function (el) { if (seen.has(el)) return; seen.add(el); el.addEventListener('mouseenter', function () { load('xylo'); play('xylo', 0.6); }); }); // reviews (static + user-submitted, added dynamically) -> delegate var lastRv = null; document.addEventListener('mouseover', function (e) { var card = e.target.closest ? e.target.closest('.rv-card') : null; if (card && card !== lastRv) { lastRv = card; load('xylo'); play('xylo', 0.6); } else if (!card) { lastRv = null; } }); } // Dynamic .vcard galleries: woodblock on 3D, xylophone on Plugins (videos only). // Cards are built by JS after load -> use event delegation. var dynKey = location.pathname.indexOf('3d.html') > -1 ? 'wood' : location.pathname.indexOf('plugins.html') > -1 ? 'xylo' : null; if (dynKey) { var lastCard = null; var videoOnly = (dynKey === 'wood'); // 3D: videos only — Plugins: every card (videos + photos) document.addEventListener('mouseover', function (e) { var card = e.target.closest ? e.target.closest('.vcard') : null; if (card && card !== lastCard) { lastCard = card; if (!videoOnly || card.querySelector('video')) { load(dynKey); play(dynKey, dynKey === 'xylo' ? 0.64 : 0.8); } } else if (!card) { lastCard = null; } }); } } // Mark the tunnel start time (used to keep the stab synced if audio unlocks late). (function () { var i = document.getElementById('intro'); if (i && getComputedStyle(i).display !== 'none' && !i.classList.contains('hidden') && !window.__introT0) { window.__introT0 = performance.now(); } })(); // Eager: create context + decode the intro stab right now so there is zero // latency when the tunnel starts; fire immediately (plays the instant audio is allowed). ensureCtx(); load('tunnel'); fireTunnel(); if (IS_WAVE) { load('wavepad'); fireWavePad(); } if (IS_CART) { load('cartopen'); fireCartOpen(); } if (IS_HEALTH) { load('healthpad'); fireHealth(); } if (IS_ORACLE) { load('oraclepad'); fireOracle(); } if (IS_INFO) { load('infopad'); fireInfo(); } if (IS_APP) { load('apppad'); fireApp(); load('pluginspad'); firePlugins(); load('threedpad'); fire3D(); load('contactspad'); fireContacts(); load('shoppad'); fireShop(); load('blackrosepad'); fireBlackRose(); load('justthispad'); fireJustThis(); } if (IS_HOME) { setupHome(); } if (document.readyState !== 'loading') attach(); else document.addEventListener('DOMContentLoaded', attach); })();