-
+
Tap tempo
@@ -303,7 +303,7 @@ function initTranspose(){
document.querySelector('#stepUp').onclick = () => { transposeSteps++; update(); };
document.querySelector('#resetTranspose').onclick = () => { transposeSteps = 0; update(); };
document.querySelector('#clearSong').onclick = () => { input.value = ''; update(); input.focus(); };
- document.querySelector('#copyResult').onclick = async () => { await navigator.clipboard?.writeText(input.value.split('\n').map(l => splitWordsAndSpaces(l).map(p => isChordToken(p) ? transposeOneToken(p, transposeSteps) : p).join('')).join('\n')).catch(()=>{}); };
+ document.querySelector('#copyResult').onclick = async () => { await navigator.clipboard?.writeText(input.value.split('\n').map(l => splitWordsAndSpaces(l).map(p => isChordToken(p) ? transposeOneToken(p, transposeSteps) : p).join('')).join('\n')); };
document.querySelector('#transposeResultInfo')?.addEventListener('click', () => openAppModal('transpose-result'));
update();
}
@@ -480,9 +480,9 @@ function updatePendulumSpeed(){
const pendulum = document.querySelector('.metro-pendulum');
if(pendulum) pendulum.style.animationDuration = `${Math.max(180, 60000 / bpm)}ms`;
}
-function updateMeter(){ const label = meter === 6 ? '6/8' : `${meter}/4`; const el=document.querySelector('#meterInDial'); if(el) el.textContent = label; const modalMeter=document.querySelector('#metroModalMeter'); if(modalMeter) modalMeter.textContent = label; }
+function updateMeter(){ const label = meter === 6 ? '6/8' : `${meter}/4`; const el=document.querySelector('#meterInDial'); if(el) el.textContent = label; const modalMeter=document.querySelector('[data-modal-meter]'); if(modalMeter) modalMeter.textContent = label; }
function renderBeats(){
- const markup = Array.from({length:meter}, (_,i) => `
${i+1}`).join('');
+ const markup = Array.from({length:meter}, (_,i) => `
${i+1}${'●'}`).join('');
const beats = document.querySelector('#beats');
if(beats) beats.innerHTML = markup;
const modalBeats = document.querySelector('#metroModalBeats');
@@ -554,7 +554,7 @@ function openChordModal(chord){
const clean = mainChord(chord);
if(!BASE_CHORDS[clean]) return;
if(!chordModal || !chordModalContent) return;
- chordModalContent.innerHTML = `
`;
+ chordModalContent.innerHTML = `
`;
chordModal.classList.add('visible'); chordModal.setAttribute('aria-hidden','false');
}
function closeChordModal(){ if(!chordModal) return; chordModal.classList.remove('visible'); chordModal.setAttribute('aria-hidden','true'); }
@@ -562,7 +562,7 @@ function openAppModal(kind){
if(!appModal || !appModalContent) return;
const installMarkup = isPwaInstalled() ? '' : '
';
appModalContent.innerHTML = kind === 'options'
- ? `
Opcje
EasyTune działa offline po pierwszym uruchomieniu.${isPwaInstalled() ? ' Aplikacja jest już uruchomiona jako zainstalowane PWA.' : ' Aby działała jako pełne PWA, wybierz „Zainstaluj aplikację”, a nie „Dodaj skrót”.'}
${installMarkup}`
+ ? `
Opcje
EasyTune działa offline po pierwszym uruchomieniu.${isPwaInstalled() ? ' Aplikacja jest już uruchomiona jako zainstalowane PWA.' : ' Aby działała jako pełne PWA, wybierz „Zainstaluj" z menu przeglądarki.'}
${installMarkup}`
: kind === 'transpose-result'
? '
Wynik transpozycji
Kliknij dowolny akord w oknie wyniku, aby otworzyć powiększony diagram tego chwytu.
'
: `
EasyTune
Transpozycja, akordy i metronom. Wersja PWA offline ${appVersion}.
`;
@@ -572,8 +572,23 @@ function openAppModal(kind){
function closeAppModal(){ if(!appModal) return; appModal.classList.remove('visible'); appModal.setAttribute('aria-hidden','true'); }
async function loadManifestVersion(){
- try{ const res = await fetch('manifest.webmanifest'); const m = await res.json(); appVersion = m.version || '1.2'; versionEl.textContent = `v${appVersion}`; } catch { versionEl.textContent = `v${appVersion}`; }
+ try {
+ const response = await fetch('./manifest.webmanifest');
+ if (!response.ok) throw new Error('Failed to fetch manifest');
+ const manifest = await response.json();
+ if (manifest.version) {
+ appVersion = manifest.version;
+ }
+ } catch (error) {
+ console.warn('Failed to load version from manifest:', error);
+ // appVersion stays as default '1.2'
+ } finally {
+ if (versionEl) {
+ versionEl.textContent = `v${appVersion}`;
+ }
+ }
}
+
function showApp(){ app.hidden = false; renderView('home'); setTimeout(()=>splash.classList.add('is-hidden'), 850); setTimeout(()=>splash.remove(), 1300); }
menuButton?.addEventListener('click', () => { const open = !optionsMenu.hidden; optionsMenu.hidden = open; menuButton.setAttribute('aria-expanded', String(!open)); });
@@ -619,7 +634,7 @@ window.matchMedia?.('(display-mode: fullscreen)')?.addEventListener?.('change',
async function promptPwaInstall(){
if (isPwaInstalled()) { syncInstallButtons(); return; }
if (!deferredInstallPrompt) {
- alert('Przeglądarka nie zgłosiła jeszcze gotowości instalacji PWA. Upewnij się, że strona działa przez HTTPS lub localhost, odśwież ją raz i wybierz w menu przeglądarki „Zainstaluj aplikację” — nie „Dodaj skrót”.');
+ alert('Przeglądarka nie zgłosiła jeszcze gotowości instalacji PWA. Upewnij się, że strona działa przez HTTPS lub localhost, odśwież ją raz i wybierz w menu przeglądarki „Zainstaluj aplikację".');
return;
}
deferredInstallPrompt.prompt();
@@ -629,1530 +644,4 @@ async function promptPwaInstall(){
syncInstallButtons();
}
syncInstallButtons();
-loadManifestVersion().finally(() => { showApp(); syncInstallButtons(); });
-
-/* =====================================================
- EasyTune v2.5 — CZYSTY METRONOM
- Ten blok nadpisuje wyłącznie widok i logikę metronomu.
- Pozostałe widoki i modale pozostają bez zmian.
- ===================================================== */
-let cleanTapTimes = [];
-let cleanMetroTimer = null;
-let metroPendulumFrame = null;
-let metroPendulumStartedAt = 0;
-let metroWasRunningBeforeModal = false;
-let metroPausedByModal = false;
-
-function metronomeHtml(){
- const meterLabel = meter === 6 ? '6/8' : `${meter}/4`;
- const accentLabel = accentMode === 'off' ? 'Brak' : accentMode === 'all' ? 'Każdy' : 'Pierwszy';
- return `
-
-
-
Metronom
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Tempo
-
-
- ${bpm}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Tap Tempo
-
Wystukaj żądane tempo, klikając kilka razy przycisk „Tap Tempo” w rytmie utworu. EasyTune automatycznie rozpozna i ustawi odpowiednią wartość BPM.
-
-
-
-
-
-
- `;
-}
-
-function initMetronome(){
- metroWasRunningBeforeModal = false;
- metroPausedByModal = false;
- renderMetroControls();
- syncCleanMetronomeUi();
- setBpm(bpm, {restart:false});
-
- document.querySelector('#startMetro')?.addEventListener('click', () => metroRunning ? stopMetronome() : startMetronome());
- document.querySelector('#metroResetTop')?.addEventListener('click', resetMetronomeSettings);
- document.querySelector('#metroTapInModal')?.addEventListener('click', handleTapTempo);
- document.querySelector('#metroBpmDown')?.addEventListener('click', () => setBpm(bpm - 1));
- document.querySelector('#metroBpmUp')?.addEventListener('click', () => setBpm(bpm + 1));
- document.querySelector('#bpmSlider')?.addEventListener('input', e => setBpm(Number(e.target.value)));
- document.querySelector('#openTempoModal')?.addEventListener('click', () => openMetroSettings('metroTempoModal'));
- document.querySelector('#openMeterModal')?.addEventListener('click', () => openMetroSettings('metroMeterModal'));
- document.querySelector('#openAccentModal')?.addEventListener('click', () => openMetroSettings('metroAccentModal'));
- document.querySelector('#metroTapInfoClean')?.addEventListener('click', () => {
- const modal = document.querySelector('#tapHelpModal');
- pauseMetronomeForModal();
- modal?.classList.add('open');
- modal?.setAttribute('aria-hidden','false');
- });
- document.querySelector('#tapHelpClose')?.addEventListener('click', closeTapHelpModal);
- document.querySelector('#tapHelpModal')?.addEventListener('click', e => {
- if(e.target === e.currentTarget) closeTapHelpModal();
- });
-
- document.querySelectorAll('[data-close-metro-settings]').forEach(btn => btn.addEventListener('click', closeMetroSettings));
- document.querySelectorAll('.metro-settings-modal').forEach(modal => modal.addEventListener('click', e => {
- if(e.target === modal) closeMetroSettings();
- }));
-}
-
-function pauseMetronomeForModal(){
- if(metroPausedByModal) return;
- metroWasRunningBeforeModal = metroRunning;
- if(metroRunning) stopMetronome();
- metroPausedByModal = metroWasRunningBeforeModal;
-}
-function isAnyMetroModalOpen(){
- const settingsOpen = Array.from(document.querySelectorAll('.metro-settings-modal')).some(m => m.classList.contains('visible'));
- const tapHelpOpen = document.querySelector('#tapHelpModal')?.classList.contains('open');
- return settingsOpen || Boolean(tapHelpOpen);
-}
-function resumeMetronomeAfterModal(){
- if(isAnyMetroModalOpen()) return;
- const shouldResume = metroPausedByModal && metroWasRunningBeforeModal;
- metroPausedByModal = false;
- metroWasRunningBeforeModal = false;
- if(shouldResume && !metroRunning) startMetronome();
-}
-
-function openMetroSettings(id){
- pauseMetronomeForModal();
- document.querySelectorAll('.metro-settings-modal').forEach(m => {
- m.classList.remove('visible');
- m.setAttribute('aria-hidden','true');
- });
- const modal = document.querySelector('#' + id);
- if(!modal) return;
- modal.classList.add('visible');
- modal.setAttribute('aria-hidden','false');
-}
-function closeMetroSettings(){
- document.querySelectorAll('.metro-settings-modal').forEach(m => {
- m.classList.remove('visible');
- m.setAttribute('aria-hidden','true');
- });
- resumeMetronomeAfterModal();
-}
-
-function closeTapHelpModal(){
- const modal = document.querySelector('#tapHelpModal');
- if(!modal) return;
- modal.classList.remove('open');
- modal.setAttribute('aria-hidden','true');
- resumeMetronomeAfterModal();
-}
-
-function renderMetroControls(){
- const presets = [60, 80, 100, 120, 140, 160];
- const presetGrid = document.querySelector('#presetGrid');
- if(presetGrid){
- presetGrid.innerHTML = presets.map(v => `
`).join('');
- presetGrid.onclick = e => {
- const btn = e.target.closest('[data-bpm]');
- if(!btn) return;
- setBpm(Number(btn.dataset.bpm));
- renderMetroControls();
- };
- }
-
- const meterButtons = document.querySelector('#meterButtons');
- if(meterButtons){
- meterButtons.innerHTML = [2,3,4,5,6].map(v => `
`).join('');
- meterButtons.onclick = e => {
- const btn = e.target.closest('[data-meter]');
- if(!btn) return;
- meter = Number(btn.dataset.meter);
- beatIndex = 0;
- saveMetroSettings();
- renderMetroControls();
- renderBeats();
- syncCleanMetronomeUi();
- };
- }
-
- const accentButtons = document.querySelector('#accentButtons');
- if(accentButtons){
- accentButtons.innerHTML = [
- ['first','Pierwszy'],
- ['all','Każdy'],
- ['off','Brak']
- ].map(([v,l]) => `
`).join('');
- accentButtons.onclick = e => {
- const btn = e.target.closest('[data-accent]');
- if(!btn) return;
- accentMode = btn.dataset.accent;
- beatIndex = 0;
- saveMetroSettings();
- renderMetroControls();
- renderBeats();
- syncCleanMetronomeUi();
- };
- }
-
- renderBeats();
- syncCleanMetronomeUi();
-}
-
-
-function stopPendulumMotion(reset=true){
- if(metroPendulumFrame){
- cancelAnimationFrame(metroPendulumFrame);
- metroPendulumFrame = null;
- }
- if(reset){
- const pendulum = document.querySelector('.metro-clean-pendulum');
- if(pendulum) pendulum.style.setProperty('transform', 'translateX(-50%) rotate(0deg)', 'important');
- }
-}
-
-function startPendulumMotion(){
- stopPendulumMotion(false);
- const pendulum = document.querySelector('.metro-clean-pendulum');
- if(!pendulum) return;
- const beatMs = Math.max(180, 60000 / bpm);
- const maxAngle = 38;
- metroPendulumStartedAt = performance.now();
-
- const draw = (now) => {
- if(!metroRunning){
- metroPendulumFrame = null;
- return;
- }
-
- // Jeden mechanizm ruchu: pozycja zależy wyłącznie od czasu i BPM.
- // Uderzenia wypadają na skrajach wahadła: lewo, prawo, lewo...
- const elapsed = now - metroPendulumStartedAt;
- const phase = (elapsed % (beatMs * 2)) / beatMs;
- const angle = phase <= 1
- ? -maxAngle + (phase * 2 * maxAngle)
- : maxAngle - ((phase - 1) * 2 * maxAngle);
-
- pendulum.style.setProperty('transform', `translateX(-50%) rotate(${angle.toFixed(2)}deg)`, 'important');
- metroPendulumFrame = requestAnimationFrame(draw);
- };
-
- pendulum.style.setProperty('transform', `translateX(-50%) rotate(${-maxAngle}deg)`, 'important');
- metroPendulumFrame = requestAnimationFrame(draw);
-}
-
-function syncCleanMetronomeUi(){
- const meterLabel = meter === 6 ? '6/8' : `${meter}/4`;
- const accentLabel = accentMode === 'off' ? 'Brak' : accentMode === 'all' ? 'Każdy' : 'Pierwszy';
-
- const bpmText = document.querySelector('#bpmValue');
- const modalBpm = document.querySelector('#metroModalBpm');
- const slider = document.querySelector('#bpmSlider');
- const tempoMeta = document.querySelector('#metroTempoMeta');
- const meterMeta = document.querySelector('#meterInDial');
- const accentMeta = document.querySelector('#metroAccentMeta');
- const visual = document.querySelector('#metroCleanVisual');
- const start = document.querySelector('#startMetro');
-
- if(bpmText) bpmText.textContent = bpm;
- if(modalBpm) modalBpm.textContent = bpm;
- if(slider) slider.value = bpm;
- if(tempoMeta) tempoMeta.textContent = `${bpm} BPM`;
- if(meterMeta) meterMeta.textContent = meterLabel;
- if(accentMeta) accentMeta.textContent = accentLabel;
- if(start) start.textContent = metroRunning ? 'Stop' : 'Start';
-
- if(visual){
- // Jedyny ruch metronomu: wahadło synchronizowane z interwałem BPM.
- visual.style.setProperty('--metro-duration', `${60000 / bpm}ms`);
- visual.classList.toggle('running', metroRunning);
- }
-}
-
-function setBpm(v, opts={restart:true}){
- bpm = Math.max(40, Math.min(240, Math.round(v)));
- saveMetroSettings();
- syncCleanMetronomeUi();
- document.querySelectorAll('.preset-bpm').forEach(b => b.classList.toggle('active', Number(b.dataset.bpm) === bpm));
- if(opts.restart && metroRunning){
- stopMetronome();
- startMetronome();
- }
-}
-
-function saveMetroSettings(){
- localStorage.setItem('easytune-metro-bpm', String(bpm));
- localStorage.setItem('easytune-metro-meter', String(meter));
- localStorage.setItem('easytune-metro-accent', accentMode);
-}
-
-function resetMetronomeSettings(){
- stopMetronome();
- bpm = 100;
- meter = 4;
- accentMode = 'first';
- cleanTapTimes = [];
- beatIndex = 0;
- saveMetroSettings();
- renderMetroControls();
- syncCleanMetronomeUi();
-}
-
-function handleTapTempo(){
- const now = Date.now();
- cleanTapTimes = cleanTapTimes.filter(t => now - t < 2200);
- cleanTapTimes.push(now);
- if(cleanTapTimes.length < 2) return;
- const intervals = cleanTapTimes.slice(1).map((t,i)=> t - cleanTapTimes[i]);
- const avg = intervals.reduce((a,b)=>a+b,0) / intervals.length;
- setBpm(Math.round(60000 / avg));
-}
-
-function renderBeats(){
- const beats = document.querySelector('#beats');
- if(!beats) return;
- beats.innerHTML = Array.from({length:meter}, (_,i)=>{
- const active = i === beatIndex;
- const accent = accentMode === 'all' || (accentMode === 'first' && i === 0);
- return `
${i+1}`;
- }).join('');
-}
-
-function ensureAudio(){
- if(!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
- if(audioCtx.state === 'suspended') audioCtx.resume();
-}
-function clickSound(accented){
- ensureAudio();
- const osc = audioCtx.createOscillator();
- const gain = audioCtx.createGain();
- osc.type = 'square';
- osc.frequency.value = accented ? 1300 : 820;
- gain.gain.setValueAtTime(0.0001, audioCtx.currentTime);
- gain.gain.exponentialRampToValueAtTime(accented ? 0.22 : 0.13, audioCtx.currentTime + 0.006);
- gain.gain.exponentialRampToValueAtTime(0.0001, audioCtx.currentTime + 0.07);
- osc.connect(gain).connect(audioCtx.destination);
- osc.start();
- osc.stop(audioCtx.currentTime + 0.08);
-}
-function tick(){
- const accented = accentMode === 'all' || (accentMode === 'first' && beatIndex === 0);
- clickSound(accented);
- renderBeats();
- const visual = document.querySelector('#metroCleanVisual');
- if(visual){
- visual.classList.remove('beat-hit','accent-hit');
- if(accented){
- void visual.offsetWidth;
- visual.classList.add('accent-hit');
- window.setTimeout(()=>visual.classList.remove('accent-hit'), 220);
- }
- }
- beatIndex = (beatIndex + 1) % meter;
-}
-function startMetronome(){
- ensureAudio();
- metroRunning = true;
- beatIndex = 0;
- clearInterval(cleanMetroTimer);
- clearInterval(metroTimer);
- cleanMetroTimer = null;
- metroTimer = null;
- const visual = document.querySelector('#metroCleanVisual');
- if(visual){
- visual.classList.remove('beat-hit','accent-hit');
- visual.style.setProperty('--metro-duration', `${60000 / bpm}ms`);
- }
- syncCleanMetronomeUi();
- startPendulumMotion();
- tick();
- cleanMetroTimer = setInterval(tick, 60000 / bpm);
- metroTimer = cleanMetroTimer;
-}
-function stopMetronome(){
- metroRunning = false;
- clearInterval(cleanMetroTimer);
- clearInterval(metroTimer);
- cleanMetroTimer = null;
- metroTimer = null;
- stopPendulumMotion(true);
- syncCleanMetronomeUi();
- renderBeats();
-}
-
-
-// v2.5.6 help modal
-document.addEventListener('click', function(e){
- if(e.target && e.target.id==='tapHelpModal'){
- e.target.classList.remove('open');
- }
-});
-
-/* EasyTune v40 — metronome unified clock: one trigger for sound, dots, accent light and pendulum */
-let metroUnifiedBeatCount = 0;
-let metroUnifiedStartAt = 0;
-
-function stopPendulumMotion(reset=true){
- if(metroPendulumFrame){
- cancelAnimationFrame(metroPendulumFrame);
- metroPendulumFrame = null;
- }
- const pendulum = document.querySelector('.metro-clean-pendulum');
- if(reset && pendulum){
- pendulum.style.setProperty('transform', 'translateX(-50%) rotate(0deg)', 'important');
- }
-}
-
-function startPendulumMotion(startAt = performance.now()){
- stopPendulumMotion(false);
- const pendulum = document.querySelector('.metro-clean-pendulum');
- if(!pendulum) return;
- const beatMs = Math.max(180, 60000 / bpm);
- const maxAngle = 38;
- metroPendulumStartedAt = startAt;
-
- const draw = (now) => {
- if(!metroRunning){
- metroPendulumFrame = null;
- return;
- }
- const elapsed = Math.max(0, now - metroPendulumStartedAt);
- const phase = (elapsed % (beatMs * 2)) / beatMs;
- const angle = phase <= 1
- ? -maxAngle + (phase * 2 * maxAngle)
- : maxAngle - ((phase - 1) * 2 * maxAngle);
- pendulum.style.setProperty('transform', `translateX(-50%) rotate(${angle.toFixed(2)}deg)`, 'important');
- metroPendulumFrame = requestAnimationFrame(draw);
- };
-
- pendulum.style.setProperty('transform', `translateX(-50%) rotate(${-maxAngle}deg)`, 'important');
- metroPendulumFrame = requestAnimationFrame(draw);
-}
-
-function metronomeBeat(){
- if(!metroRunning) return;
- const accented = accentMode === 'all' || (accentMode === 'first' && beatIndex === 0);
- clickSound(accented);
- renderBeats();
- const visual = document.querySelector('#metroCleanVisual');
- if(visual){
- visual.classList.remove('beat-hit','accent-hit');
- if(accented){
- void visual.offsetWidth;
- visual.classList.add('accent-hit');
- window.setTimeout(()=>visual.classList.remove('accent-hit'), 180);
- }
- }
- beatIndex = (beatIndex + 1) % meter;
-}
-
-function tick(){
- metronomeBeat();
-}
-
-function scheduleNextMetronomeBeat(){
- if(!metroRunning) return;
- const beatMs = Math.max(180, 60000 / bpm);
- metroUnifiedBeatCount += 1;
- const dueAt = metroUnifiedStartAt + metroUnifiedBeatCount * beatMs;
- const delay = Math.max(0, dueAt - performance.now());
- cleanMetroTimer = window.setTimeout(() => {
- metronomeBeat();
- scheduleNextMetronomeBeat();
- }, delay);
- metroTimer = cleanMetroTimer;
-}
-
-function startMetronome(){
- ensureAudio();
- stopMetronome();
- metroRunning = true;
- beatIndex = 0;
- metroUnifiedBeatCount = 0;
- metroUnifiedStartAt = performance.now();
- const visual = document.querySelector('#metroCleanVisual');
- if(visual){
- visual.classList.remove('beat-hit','accent-hit');
- visual.style.setProperty('--metro-duration', `${60000 / bpm}ms`);
- }
- syncCleanMetronomeUi();
- startPendulumMotion(metroUnifiedStartAt);
- metronomeBeat();
- scheduleNextMetronomeBeat();
-}
-
-function stopMetronome(){
- metroRunning = false;
- if(cleanMetroTimer) window.clearTimeout(cleanMetroTimer);
- if(metroTimer) window.clearTimeout(metroTimer);
- window.clearInterval(cleanMetroTimer);
- window.clearInterval(metroTimer);
- cleanMetroTimer = null;
- metroTimer = null;
- stopPendulumMotion(true);
- syncCleanMetronomeUi();
- renderBeats();
-}
-
-/* EasyTune v41 — metronome: extrema-driven single trigger
- Sound, accent light and beat dots fire ONLY when the needle is at max left/right.
- The circle never pulses. The needle transition between extrema is visual only. */
-let metroExtremeSide = -1;
-let metroMoveTimer = null;
-let metroMoveSerial = 0;
-
-function metroBeatMs(){
- return Math.max(180, 60000 / bpm);
-}
-
-function setMetroNeedleSide(side, immediate=false){
- const pendulum = document.querySelector('.metro-clean-pendulum');
- if(!pendulum) return;
- const angle = side < 0 ? -38 : 38;
- if(immediate){
- pendulum.style.setProperty('transition', 'none', 'important');
- pendulum.style.setProperty('transform', `translateX(-50%) rotate(${angle}deg)`, 'important');
- pendulum.getBoundingClientRect();
- } else {
- pendulum.style.setProperty('transition', `transform ${metroBeatMs()}ms cubic-bezier(.37,0,.63,1)`, 'important');
- pendulum.style.setProperty('transform', `translateX(-50%) rotate(${angle}deg)`, 'important');
- }
-}
-
-function clearMetroClock(){
- metroMoveSerial += 1;
- if(metroMoveTimer) window.clearTimeout(metroMoveTimer);
- if(cleanMetroTimer) window.clearTimeout(cleanMetroTimer);
- if(metroTimer) window.clearTimeout(metroTimer);
- window.clearInterval(cleanMetroTimer);
- window.clearInterval(metroTimer);
- metroMoveTimer = null;
- cleanMetroTimer = null;
- metroTimer = null;
- if(metroPendulumFrame){
- cancelAnimationFrame(metroPendulumFrame);
- metroPendulumFrame = null;
- }
-}
-
-function fireMetroExtremeBeat(){
- if(!metroRunning) return;
- const accented = accentMode === 'all' || (accentMode === 'first' && beatIndex === 0);
- clickSound(accented);
- renderBeats();
-
- const visual = document.querySelector('#metroCleanVisual');
- if(visual){
- visual.classList.remove('beat-hit','accent-hit');
- if(accented){
- void visual.offsetWidth;
- visual.classList.add('accent-hit');
- window.setTimeout(() => visual.classList.remove('accent-hit'), 180);
- }
- }
- beatIndex = (beatIndex + 1) % meter;
-}
-
-function scheduleMetroExtremeLoop(serial){
- if(!metroRunning || serial !== metroMoveSerial) return;
- const beatMs = metroBeatMs();
- const nextSide = -metroExtremeSide;
-
- // After the beat fires at the current extreme, move toward the next extreme.
- requestAnimationFrame(() => {
- if(!metroRunning || serial !== metroMoveSerial) return;
- setMetroNeedleSide(nextSide, false);
- });
-
- metroMoveTimer = window.setTimeout(() => {
- if(!metroRunning || serial !== metroMoveSerial) return;
- metroExtremeSide = nextSide;
- // Confirm exact endpoint before firing the next beat.
- setMetroNeedleSide(metroExtremeSide, true);
- fireMetroExtremeBeat();
- scheduleMetroExtremeLoop(serial);
- }, beatMs);
- cleanMetroTimer = metroMoveTimer;
- metroTimer = metroMoveTimer;
-}
-
-function tick(){
- fireMetroExtremeBeat();
-}
-
-function stopPendulumMotion(reset=true){
- clearMetroClock();
- if(reset){
- const pendulum = document.querySelector('.metro-clean-pendulum');
- if(pendulum){
- pendulum.style.setProperty('transition','none','important');
- pendulum.style.setProperty('transform','translateX(-50%) rotate(0deg)','important');
- }
- }
-}
-
-function startPendulumMotion(){
- // Kept for compatibility; real motion is driven by startMetronome().
- setMetroNeedleSide(metroExtremeSide, true);
-}
-
-function startMetronome(){
- ensureAudio();
- stopMetronome();
- metroRunning = true;
- beatIndex = 0;
- metroExtremeSide = -1;
- metroMoveSerial += 1;
- const serial = metroMoveSerial;
-
- const visual = document.querySelector('#metroCleanVisual');
- if(visual){
- visual.classList.remove('beat-hit','accent-hit');
- visual.style.setProperty('--metro-duration', `${metroBeatMs()}ms`);
- }
-
- setMetroNeedleSide(metroExtremeSide, true);
- syncCleanMetronomeUi();
- fireMetroExtremeBeat();
- scheduleMetroExtremeLoop(serial);
-}
-
-function stopMetronome(){
- metroRunning = false;
- clearMetroClock();
- const pendulum = document.querySelector('.metro-clean-pendulum');
- if(pendulum){
- pendulum.style.setProperty('transition','none','important');
- pendulum.style.setProperty('transform','translateX(-50%) rotate(0deg)','important');
- }
- syncCleanMetronomeUi();
- renderBeats();
-}
-
-let metroEndpointTimer = null;
-
-/* EasyTune v45 — metronome: single RAF clock, sound/light at needle extrema.
- No CSS swing, no circle pulse, no frame flash. The same animation frame sets
- the needle endpoint and fires the sound/accent light. */
-let metroRafId = null;
-let metroRafStart = 0;
-let metroLastBeatOrdinal = -1;
-let metroRunSerial = 0;
-
-function metroBeatMs(){
- return Math.max(180, 60000 / bpm);
-}
-
-function metroMaxAngle(){ return 38; }
-
-function setMetroNeedleAngle(angle){
- const pendulum = document.querySelector('.metro-clean-pendulum');
- if(!pendulum) return;
- pendulum.style.setProperty('animation', 'none', 'important');
- pendulum.style.setProperty('transition', 'none', 'important');
- pendulum.style.setProperty('transform-origin', '50% 100%', 'important');
- pendulum.style.setProperty('transform', `translateX(-50%) rotate(${angle}deg)`, 'important');
-}
-
-function setMetroNeedleExtreme(side){
- setMetroNeedleAngle(side < 0 ? -metroMaxAngle() : metroMaxAngle());
-}
-
-function clearMetroEndpointClock(){
- metroRunSerial += 1;
- if(metroRafId){
- cancelAnimationFrame(metroRafId);
- metroRafId = null;
- }
- if(cleanMetroTimer) window.clearTimeout(cleanMetroTimer);
- if(metroTimer) window.clearTimeout(metroTimer);
- window.clearInterval(cleanMetroTimer);
- window.clearInterval(metroTimer);
- cleanMetroTimer = null;
- metroTimer = null;
- if(typeof metroEndpointTimer !== 'undefined' && metroEndpointTimer) window.clearTimeout(metroEndpointTimer);
- metroEndpointTimer = null;
- if(metroPendulumFrame){
- cancelAnimationFrame(metroPendulumFrame);
- metroPendulumFrame = null;
- }
-}
-
-function fireMetroBeatFromNeedle(beatOrdinal){
- if(!metroRunning) return;
- const side = beatOrdinal % 2 === 0 ? -1 : 1;
- setMetroNeedleExtreme(side);
- const accented = accentMode === 'all' || (accentMode === 'first' && beatIndex === 0);
- clickSound(accented);
- renderBeats();
- const visual = document.querySelector('#metroCleanVisual');
- if(visual){
- visual.classList.remove('beat-hit','accent-hit');
- if(accented){
- void visual.offsetWidth;
- visual.classList.add('accent-hit');
- window.setTimeout(() => visual.classList.remove('accent-hit'), 150);
- }
- }
- beatIndex = (beatIndex + 1) % meter;
-}
-
-function metroAnimationFrame(serial, now){
- if(!metroRunning || serial !== metroRunSerial) return;
- const beatMs = metroBeatMs();
- const elapsed = Math.max(0, now - metroRafStart);
- const beatOrdinal = Math.floor(elapsed / beatMs);
-
- if(beatOrdinal !== metroLastBeatOrdinal){
- metroLastBeatOrdinal = beatOrdinal;
- fireMetroBeatFromNeedle(beatOrdinal);
- } else {
- // Left extreme at beat 0, right extreme at beat 1, etc.
- const angle = -metroMaxAngle() * Math.cos(Math.PI * elapsed / beatMs);
- setMetroNeedleAngle(angle);
- }
-
- metroRafId = requestAnimationFrame(t => metroAnimationFrame(serial, t));
-}
-
-function startMetronome(){
- ensureAudio();
- clearMetroEndpointClock();
- metroRunning = true;
- beatIndex = 0;
- metroLastBeatOrdinal = -1;
- const serial = metroRunSerial;
- const visual = document.querySelector('#metroCleanVisual');
- if(visual){
- visual.classList.remove('beat-hit','accent-hit');
- visual.classList.add('running');
- visual.style.setProperty('--metro-duration', `${metroBeatMs()}ms`);
- }
-
- // Start visibly at the left endpoint, then the RAF clock becomes the single source of truth.
- setMetroNeedleExtreme(-1);
- syncCleanMetronomeUi();
- requestAnimationFrame(() => {
- requestAnimationFrame(t => {
- if(!metroRunning || serial !== metroRunSerial) return;
- metroRafStart = t;
- metroRafId = requestAnimationFrame(tt => metroAnimationFrame(serial, tt));
- });
- });
-}
-
-function stopMetronome(){
- metroRunning = false;
- clearMetroEndpointClock();
- setMetroNeedleAngle(0);
- const visual = document.querySelector('#metroCleanVisual');
- if(visual) visual.classList.remove('running','beat-hit','accent-hit');
- syncCleanMetronomeUi();
- renderBeats();
-}
-
-function tick(){
- fireMetroBeatFromNeedle(Math.max(0, metroLastBeatOrdinal + 1));
-}
-
-function startPendulumMotion(){
- if(!metroRunning) setMetroNeedleExtreme(-1);
-}
-
-function stopPendulumMotion(reset=true){
- clearMetroEndpointClock();
- if(reset) setMetroNeedleAngle(0);
-}
-
-/* EasyTune v47 — FINAL metronome phase fix.
- Start needle at LEFT extreme. Do NOT click at start.
- First click/dot/light fires only when the needle reaches RIGHT extreme,
- then on every next extreme. Circle/frame pulse stays disabled by CSS. */
-function metroAnimationFrame(serial, now){
- if(!metroRunning || serial !== metroRunSerial) return;
- const beatMs = metroBeatMs();
- const elapsed = Math.max(0, now - metroRafStart);
-
- // Position is the source of truth: t=0 left, t=beatMs right, t=2*beatMs left.
- const angle = -metroMaxAngle() * Math.cos(Math.PI * elapsed / beatMs);
- setMetroNeedleAngle(angle);
-
- const beatOrdinal = Math.floor(elapsed / beatMs);
- if(beatOrdinal >= 1 && beatOrdinal !== metroLastBeatOrdinal){
- metroLastBeatOrdinal = beatOrdinal;
- fireMetroBeatFromNeedle(beatOrdinal);
- }
-
- metroRafId = requestAnimationFrame(t => metroAnimationFrame(serial, t));
-}
-
-function startMetronome(){
- ensureAudio();
- clearMetroEndpointClock();
- metroRunning = true;
- beatIndex = 0;
- metroLastBeatOrdinal = 0; // start is visual only; first audible beat is at RIGHT extreme
- const serial = metroRunSerial;
- const visual = document.querySelector('#metroCleanVisual');
- if(visual){
- visual.classList.remove('beat-hit','accent-hit');
- visual.classList.add('running');
- visual.style.setProperty('--metro-duration', `${metroBeatMs()}ms`);
- }
-
- setMetroNeedleExtreme(-1);
- renderBeats();
- syncCleanMetronomeUi();
-
- requestAnimationFrame(t => {
- if(!metroRunning || serial !== metroRunSerial) return;
- metroRafStart = t;
- metroRafId = requestAnimationFrame(tt => metroAnimationFrame(serial, tt));
- });
-}
-
-function stopMetronome(){
- metroRunning = false;
- clearMetroEndpointClock();
- setMetroNeedleAngle(0);
- const visual = document.querySelector('#metroCleanVisual');
- if(visual) visual.classList.remove('running','beat-hit','accent-hit');
- syncCleanMetronomeUi();
- renderBeats();
-}
-
-/* EasyTune v48 — metronome endpoint clock final override.
- Idle/start needle is LEFT. First sound/dot fires only after the needle reaches RIGHT.
- A single timeout-driven endpoint loop controls needle, sound, dots and accent light. */
-let metroEndpointLoopTimer = null;
-let metroEndpointSerialFinal = 0;
-let metroEndpointSideFinal = -1;
-
-function metroBeatMs(){
- return Math.max(180, 60000 / bpm);
-}
-function metroMaxAngle(){ return 38; }
-function metroAngleForSide(side){ return side < 0 ? -metroMaxAngle() : metroMaxAngle(); }
-
-function setMetroNeedleEndpoint(side, animate=false){
- const pendulum = document.querySelector('.metro-clean-pendulum');
- if(!pendulum) return;
- pendulum.style.setProperty('animation', 'none', 'important');
- pendulum.style.setProperty('transform-origin', '50% 100%', 'important');
- pendulum.style.setProperty('transition', animate ? `transform ${metroBeatMs()}ms cubic-bezier(.37,0,.63,1)` : 'none', 'important');
- pendulum.style.setProperty('transform', `translateX(-50%) rotate(${metroAngleForSide(side)}deg)`, 'important');
-}
-
-function clearMetroEndpointLoopFinal(){
- metroEndpointSerialFinal += 1;
- if(metroEndpointLoopTimer) window.clearTimeout(metroEndpointLoopTimer);
- metroEndpointLoopTimer = null;
- if(cleanMetroTimer) window.clearTimeout(cleanMetroTimer);
- if(metroTimer) window.clearTimeout(metroTimer);
- if(cleanMetroTimer) window.clearInterval(cleanMetroTimer);
- if(metroTimer) window.clearInterval(metroTimer);
- cleanMetroTimer = null;
- metroTimer = null;
- if(metroRafId){ cancelAnimationFrame(metroRafId); metroRafId = null; }
- if(metroPendulumFrame){ cancelAnimationFrame(metroPendulumFrame); metroPendulumFrame = null; }
-}
-
-function fireMetroEndpointBeatFinal(){
- if(!metroRunning) return;
- const accented = accentMode === 'all' || (accentMode === 'first' && beatIndex === 0);
- clickSound(accented);
- renderBeats();
- const visual = document.querySelector('#metroCleanVisual');
- if(visual){
- visual.classList.remove('beat-hit','accent-hit');
- if(accented){
- void visual.offsetWidth;
- visual.classList.add('accent-hit');
- window.setTimeout(() => visual.classList.remove('accent-hit'), 150);
- }
- }
- beatIndex = (beatIndex + 1) % meter;
-}
-
-function runMetroEndpointLoopFinal(serial){
- if(!metroRunning || serial !== metroEndpointSerialFinal) return;
- const beatMs = metroBeatMs();
- const nextSide = -metroEndpointSideFinal;
-
- // Move first. The beat happens only after the visual needle has arrived.
- requestAnimationFrame(() => {
- if(!metroRunning || serial !== metroEndpointSerialFinal) return;
- setMetroNeedleEndpoint(nextSide, true);
- });
-
- metroEndpointLoopTimer = window.setTimeout(() => {
- if(!metroRunning || serial !== metroEndpointSerialFinal) return;
- metroEndpointSideFinal = nextSide;
- setMetroNeedleEndpoint(metroEndpointSideFinal, false); // snap to exact edge before click
- fireMetroEndpointBeatFinal();
- runMetroEndpointLoopFinal(serial);
- }, beatMs);
- cleanMetroTimer = metroEndpointLoopTimer;
- metroTimer = metroEndpointLoopTimer;
-}
-
-function startMetronome(){
- ensureAudio();
- clearMetroEndpointLoopFinal();
- metroRunning = true;
- beatIndex = 0;
- metroEndpointSideFinal = -1;
- const serial = metroEndpointSerialFinal;
- const visual = document.querySelector('#metroCleanVisual');
- if(visual){
- visual.classList.remove('beat-hit','accent-hit');
- visual.classList.add('running');
- visual.style.setProperty('--metro-duration', `${metroBeatMs()}ms`);
- }
- setMetroNeedleEndpoint(-1, false); // visible LEFT start; no sound here
- renderBeats();
- syncCleanMetronomeUi();
- runMetroEndpointLoopFinal(serial);
-}
-
-function stopMetronome(){
- metroRunning = false;
- clearMetroEndpointLoopFinal();
- metroEndpointSideFinal = -1;
- setMetroNeedleEndpoint(-1, false);
- const visual = document.querySelector('#metroCleanVisual');
- if(visual) visual.classList.remove('running','beat-hit','accent-hit');
- syncCleanMetronomeUi();
- beatIndex = 0;
- renderBeats();
-}
-
-function tick(){
- fireMetroEndpointBeatFinal();
-}
-function startPendulumMotion(){
- if(!metroRunning) setMetroNeedleEndpoint(-1, false);
-}
-function stopPendulumMotion(reset=true){
- clearMetroEndpointLoopFinal();
- if(reset) setMetroNeedleEndpoint(-1, false);
-}
-
-/* EasyTune v50 — metronom napisany od nowa: start = maksymalny lewy wychył.
- Jeden zegar endpointów. Brak dźwięku na starcie. Pierwsze piknięcie dopiero
- po dojściu wskazówki do prawego skraju. */
-(function(){
- const LEFT = -1;
- const RIGHT = 1;
- const MAX_ANGLE = 38;
- let endpointTimer = null;
- let endpointSerial = 0;
- let endpointSide = LEFT;
-
- function msPerBeat(){ return Math.max(180, 60000 / bpm); }
- function angleFor(side){ return side === LEFT ? -MAX_ANGLE : MAX_ANGLE; }
-
- function needle(){ return document.querySelector('.metro-clean-pendulum'); }
- function visual(){ return document.querySelector('#metroCleanVisual'); }
-
- function forceNeedle(side){
- const p = needle();
- if(!p) return;
- p.style.setProperty('animation', 'none', 'important');
- p.style.setProperty('transition', 'none', 'important');
- p.style.setProperty('transform-origin', '50% 100%', 'important');
- p.style.setProperty('transform', `translateX(-50%) rotate(${angleFor(side)}deg)`, 'important');
- }
-
- function moveNeedle(side){
- const p = needle();
- if(!p) return;
- p.style.setProperty('animation', 'none', 'important');
- p.style.setProperty('transition', `transform ${msPerBeat()}ms cubic-bezier(.37,0,.63,1)`, 'important');
- p.style.setProperty('transform-origin', '50% 100%', 'important');
- p.style.setProperty('transform', `translateX(-50%) rotate(${angleFor(side)}deg)`, 'important');
- }
-
- function clearMetronomeClock(){
- endpointSerial += 1;
- if(endpointTimer) window.clearTimeout(endpointTimer);
- endpointTimer = null;
- if(cleanMetroTimer) { window.clearTimeout(cleanMetroTimer); window.clearInterval(cleanMetroTimer); }
- if(metroTimer) { window.clearTimeout(metroTimer); window.clearInterval(metroTimer); }
- cleanMetroTimer = null;
- metroTimer = null;
- if(metroPendulumFrame){ cancelAnimationFrame(metroPendulumFrame); metroPendulumFrame = null; }
- if(typeof metroRafId !== 'undefined' && metroRafId){ cancelAnimationFrame(metroRafId); metroRafId = null; }
- }
-
- function fireBeatAtCurrentEdge(){
- if(!metroRunning) return;
- const accented = accentMode === 'all' || (accentMode === 'first' && beatIndex === 0);
- clickSound(accented);
- renderBeats();
- const v = visual();
- if(v){
- v.classList.remove('beat-hit','accent-hit');
- if(accented){
- void v.offsetWidth;
- v.classList.add('accent-hit');
- window.setTimeout(() => v.classList.remove('accent-hit'), 150);
- }
- }
- beatIndex = (beatIndex + 1) % meter;
- }
-
- function scheduleMove(serial){
- if(!metroRunning || serial !== endpointSerial) return;
- const target = endpointSide === LEFT ? RIGHT : LEFT;
- moveNeedle(target);
-
- endpointTimer = window.setTimeout(() => {
- if(!metroRunning || serial !== endpointSerial) return;
- endpointSide = target;
- forceNeedle(endpointSide); // dokładny skraj PRZED dźwiękiem
- fireBeatAtCurrentEdge(); // dźwięk/kropka/światło wyłącznie na skraju
- scheduleMove(serial);
- }, msPerBeat());
-
- cleanMetroTimer = endpointTimer;
- metroTimer = endpointTimer;
- }
-
- window.setMetronomeNeedleLeft = function(){
- endpointSide = LEFT;
- forceNeedle(LEFT);
- };
-
- // Nadpisanie poprzednich implementacji.
- startMetronome = function(){
- ensureAudio();
- clearMetronomeClock();
- metroRunning = true;
- beatIndex = 0;
- endpointSide = LEFT;
- const serial = endpointSerial;
- const v = visual();
- if(v){
- v.classList.remove('beat-hit','accent-hit');
- v.classList.add('running');
- v.style.setProperty('--metro-duration', `${msPerBeat()}ms`);
- }
- forceNeedle(LEFT); // START: maksymalny lewy wychył
- renderBeats();
- syncCleanMetronomeUi();
- requestAnimationFrame(() => scheduleMove(serial)); // pierwsze piknięcie po dojściu do prawego skraju
- };
-
- stopMetronome = function(){
- metroRunning = false;
- clearMetronomeClock();
- endpointSide = LEFT;
- forceNeedle(LEFT); // STOP/idle: nadal maksymalny lewy wychył
- const v = visual();
- if(v) v.classList.remove('running','beat-hit','accent-hit');
- beatIndex = 0;
- syncCleanMetronomeUi();
- renderBeats();
- };
-
- startPendulumMotion = function(){ forceNeedle(LEFT); };
- stopPendulumMotion = function(){ clearMetronomeClock(); forceNeedle(LEFT); };
- tick = function(){ fireBeatAtCurrentEdge(); };
-
- const previousInitMetronome = initMetronome;
- initMetronome = function(){
- previousInitMetronome();
- if(!metroRunning) requestAnimationFrame(() => forceNeedle(LEFT));
- };
-})();
-
-/* EasyTune v51 — METRONOME FINAL: RAF is the only clock.
- Start/idle needle is max LEFT. No sound at start. First beat fires at max RIGHT.
- Sound, dots and accent light are triggered in the same animation frame that draws the endpoint. */
-(function(){
- const LEFT = -1;
- const RIGHT = 1;
- const MAX_ANGLE = 38;
- let rafId = null;
- let runSerial = 0;
- let runStart = 0;
- let lastEndpoint = 0; // 0 means visual start only; first audible endpoint is 1 (RIGHT)
-
- function beatMs(){ return Math.max(180, 60000 / bpm); }
- function needle(){ return document.querySelector('.metro-clean-pendulum'); }
- function visual(){ return document.querySelector('#metroCleanVisual'); }
- function angleForEndpoint(endpoint){ return endpoint % 2 === 0 ? -MAX_ANGLE : MAX_ANGLE; }
-
- function drawNeedle(angle){
- const p = needle();
- if(!p) return;
- p.style.setProperty('animation', 'none', 'important');
- p.style.setProperty('transition', 'none', 'important');
- p.style.setProperty('transform-origin', '50% 100%', 'important');
- p.style.setProperty('transform', `translateX(-50%) rotate(${angle}deg)`, 'important');
- }
-
- function drawLeftStart(){ drawNeedle(-MAX_ANGLE); }
-
- function clearRafClock(){
- runSerial += 1;
- if(rafId){ cancelAnimationFrame(rafId); rafId = null; }
- if(cleanMetroTimer){ clearTimeout(cleanMetroTimer); clearInterval(cleanMetroTimer); cleanMetroTimer = null; }
- if(metroTimer){ clearTimeout(metroTimer); clearInterval(metroTimer); metroTimer = null; }
- if(typeof metroEndpointTimer !== 'undefined' && metroEndpointTimer){ clearTimeout(metroEndpointTimer); metroEndpointTimer = null; }
- if(typeof metroEndpointLoopTimer !== 'undefined' && metroEndpointLoopTimer){ clearTimeout(metroEndpointLoopTimer); metroEndpointLoopTimer = null; }
- if(typeof metroPendulumFrame !== 'undefined' && metroPendulumFrame){ cancelAnimationFrame(metroPendulumFrame); metroPendulumFrame = null; }
- if(typeof metroRafId !== 'undefined' && metroRafId){ cancelAnimationFrame(metroRafId); metroRafId = null; }
- }
-
- function fireBeat(){
- if(!metroRunning) return;
- const accented = accentMode === 'all' || (accentMode === 'first' && beatIndex === 0);
- clickSound(accented);
- renderBeats();
- const v = visual();
- if(v){
- v.classList.remove('beat-hit', 'accent-hit');
- if(accented){
- void v.offsetWidth;
- v.classList.add('accent-hit');
- setTimeout(() => v.classList.remove('accent-hit'), 140);
- }
- }
- beatIndex = (beatIndex + 1) % meter;
- }
-
- function frame(serial, now){
- if(!metroRunning || serial !== runSerial) return;
- const ms = beatMs();
- const elapsed = Math.max(0, now - runStart);
-
- // One half-swing per beat: 0=LEFT, 1=RIGHT, 2=LEFT...
- const endpoint = Math.floor(elapsed / ms);
- const inHalf = (elapsed - endpoint * ms) / ms;
-
- // Draw position first. At endpoint boundaries this is exactly LEFT or RIGHT.
- const angle = -MAX_ANGLE * Math.cos(Math.PI * (endpoint + inHalf));
- drawNeedle(Number(angle.toFixed(3)));
-
- // First sound is endpoint 1 (RIGHT), never endpoint 0 (LEFT start).
- if(endpoint >= 1 && endpoint !== lastEndpoint){
- lastEndpoint = endpoint;
- drawNeedle(angleForEndpoint(endpoint)); // exact endpoint before audio/UI trigger
- fireBeat();
- }
-
- rafId = requestAnimationFrame(t => frame(serial, t));
- }
-
- startMetronome = function(){
- ensureAudio();
- clearRafClock();
- metroRunning = true;
- beatIndex = 0;
- lastEndpoint = 0;
- const serial = runSerial;
- const v = visual();
- if(v){
- v.classList.add('running');
- v.classList.remove('beat-hit', 'accent-hit');
- v.style.setProperty('--metro-duration', `${beatMs()}ms`);
- }
- drawLeftStart();
- renderBeats();
- syncCleanMetronomeUi();
- requestAnimationFrame(t => {
- if(!metroRunning || serial !== runSerial) return;
- runStart = t;
- drawLeftStart();
- rafId = requestAnimationFrame(tt => frame(serial, tt));
- });
- };
-
- stopMetronome = function(){
- metroRunning = false;
- clearRafClock();
- beatIndex = 0;
- const v = visual();
- if(v) v.classList.remove('running', 'beat-hit', 'accent-hit');
- drawLeftStart();
- syncCleanMetronomeUi();
- renderBeats();
- };
-
- startPendulumMotion = drawLeftStart;
- stopPendulumMotion = function(){ clearRafClock(); drawLeftStart(); };
- tick = fireBeat;
-
- const previousInit = initMetronome;
- initMetronome = function(){
- previousInit();
- if(!metroRunning) requestAnimationFrame(drawLeftStart);
- };
-})();
-
-
-/* EasyTune v52 — metronome sync rewrite.
- One RAF draws the needle. Audio, dot and accent light are scheduled ahead for
- the exact endpoint time, so they happen when the needle reaches LEFT/RIGHT.
- Start/idle is always max LEFT. No frame/circle/border blinking. */
-(function(){
- const LEFT = -1;
- const RIGHT = 1;
- const MAX_ANGLE = 38;
- const LOOKAHEAD_MS = 90;
- let rafId = null;
- let serial = 0;
- let startTs = 0;
- let nextEndpoint = 1; // endpoint 0 is the silent LEFT start; endpoint 1 is first audible RIGHT
- let scheduledUiTimers = [];
-
- function beatMs(){ return Math.max(180, 60000 / bpm); }
- function angleAt(elapsed){ return -MAX_ANGLE * Math.cos(Math.PI * elapsed / beatMs()); }
- function needle(){ return document.querySelector('.metro-clean-pendulum'); }
- function visual(){ return document.querySelector('#metroCleanVisual'); }
-
- function setNeedle(angle){
- const p = needle();
- if(!p) return;
- p.style.setProperty('animation', 'none', 'important');
- p.style.setProperty('transition', 'none', 'important');
- p.style.setProperty('transform-origin', '50% 100%', 'important');
- p.style.setProperty('transform', `translateX(-50%) rotate(${angle.toFixed(3)}deg)`, 'important');
- }
-
- function setLeftStart(){ setNeedle(-MAX_ANGLE); }
-
- function clearAllMetronomeClocks(){
- serial += 1;
- if(rafId){ cancelAnimationFrame(rafId); rafId = null; }
- scheduledUiTimers.forEach(id => clearTimeout(id));
- scheduledUiTimers = [];
- if(cleanMetroTimer){ clearTimeout(cleanMetroTimer); clearInterval(cleanMetroTimer); cleanMetroTimer = null; }
- if(metroTimer){ clearTimeout(metroTimer); clearInterval(metroTimer); metroTimer = null; }
- if(typeof metroEndpointTimer !== 'undefined' && metroEndpointTimer){ clearTimeout(metroEndpointTimer); metroEndpointTimer = null; }
- if(typeof metroEndpointLoopTimer !== 'undefined' && metroEndpointLoopTimer){ clearTimeout(metroEndpointLoopTimer); metroEndpointLoopTimer = null; }
- if(typeof metroPendulumFrame !== 'undefined' && metroPendulumFrame){ cancelAnimationFrame(metroPendulumFrame); metroPendulumFrame = null; }
- if(typeof metroRafId !== 'undefined' && metroRafId){ cancelAnimationFrame(metroRafId); metroRafId = null; }
- }
-
- function scheduleClickSound(accented, when){
- ensureAudio();
- const osc = audioCtx.createOscillator();
- const gain = audioCtx.createGain();
- osc.type = 'square';
- osc.frequency.setValueAtTime(accented ? 1300 : 820, when);
- gain.gain.setValueAtTime(0.0001, when);
- gain.gain.exponentialRampToValueAtTime(accented ? 0.22 : 0.13, when + 0.006);
- gain.gain.exponentialRampToValueAtTime(0.0001, when + 0.075);
- osc.connect(gain).connect(audioCtx.destination);
- osc.start(when);
- osc.stop(when + 0.085);
- }
-
- function fireUiBeat(accented){
- if(!metroRunning) return;
- renderBeats();
- const v = visual();
- if(v){
- v.classList.remove('beat-hit', 'accent-hit');
- if(accented){
- void v.offsetWidth;
- v.classList.add('accent-hit');
- const off = setTimeout(() => v.classList.remove('accent-hit'), 140);
- scheduledUiTimers.push(off);
- }
- }
- beatIndex = (beatIndex + 1) % meter;
- }
-
- function scheduleEndpointBeat(endpoint, delayMs){
- const accented = accentMode === 'all' || (accentMode === 'first' && beatIndex === 0);
- const audioWhen = audioCtx.currentTime + Math.max(0, delayMs) / 1000;
- scheduleClickSound(accented, audioWhen);
- const id = setTimeout(() => fireUiBeat(accented), Math.max(0, delayMs));
- scheduledUiTimers.push(id);
- }
-
- function frame(mySerial, now){
- if(!metroRunning || mySerial !== serial) return;
- const elapsed = Math.max(0, now - startTs);
- setNeedle(angleAt(elapsed));
-
- const ms = beatMs();
- while(nextEndpoint * ms - elapsed <= LOOKAHEAD_MS){
- const delay = nextEndpoint * ms - elapsed;
- if(nextEndpoint >= 1) scheduleEndpointBeat(nextEndpoint, delay);
- nextEndpoint += 1;
- }
-
- rafId = requestAnimationFrame(t => frame(mySerial, t));
- }
-
- startMetronome = function(){
- ensureAudio();
- clearAllMetronomeClocks();
- metroRunning = true;
- beatIndex = 0;
- nextEndpoint = 1;
- const mySerial = serial;
- const v = visual();
- if(v){
- v.classList.add('running');
- v.classList.remove('beat-hit', 'accent-hit');
- v.style.setProperty('--metro-duration', `${beatMs()}ms`);
- }
- setLeftStart();
- renderBeats();
- syncCleanMetronomeUi();
- requestAnimationFrame(t => {
- if(!metroRunning || mySerial !== serial) return;
- startTs = t;
- setLeftStart();
- rafId = requestAnimationFrame(tt => frame(mySerial, tt));
- });
- };
-
- stopMetronome = function(){
- metroRunning = false;
- clearAllMetronomeClocks();
- beatIndex = 0;
- const v = visual();
- if(v) v.classList.remove('running', 'beat-hit', 'accent-hit');
- setLeftStart();
- syncCleanMetronomeUi();
- renderBeats();
- };
-
- startPendulumMotion = setLeftStart;
- stopPendulumMotion = function(){ clearAllMetronomeClocks(); setLeftStart(); };
- tick = function(){
- const accented = accentMode === 'all' || (accentMode === 'first' && beatIndex === 0);
- clickSound(accented);
- fireUiBeat(accented);
- };
-
- const prevInit = initMetronome;
- initMetronome = function(){
- prevInit();
- if(!metroRunning) requestAnimationFrame(setLeftStart);
- };
-})();
-
-/* EasyTune v53 — metronome perceptual-centre sync.
- Needle remains the visual clock. For every endpoint, sound, dot transition and
- accent light are started early by half of their own duration, so the CENTER
- of the beep / dot transition / flash lands on the exact left/right extreme.
- This compensates for fixed animation and click lengths at every BPM. */
-(function(){
- const MAX_ANGLE = 38;
- const CLICK_DUR_MS = 85;
- const DOT_TRANSITION_MS = 80;
- const ACCENT_FLASH_MS = 140;
- const SCHED_LOOKAHEAD_MS = 180;
- let rafId = null;
- let serial = 0;
- let startTs = 0;
- let nextEndpoint = 1;
- let timers = [];
-
- function beatMs(){ return Math.max(180, 60000 / bpm); }
- function needle(){ return document.querySelector('.metro-clean-pendulum'); }
- function visual(){ return document.querySelector('#metroCleanVisual'); }
- function endpointTimeMs(endpoint){ return endpoint * beatMs(); }
- function angleAtElapsed(elapsed){ return -MAX_ANGLE * Math.cos(Math.PI * elapsed / beatMs()); }
- function angleForEndpoint(endpoint){ return endpoint % 2 === 0 ? -MAX_ANGLE : MAX_ANGLE; }
-
- function addTimer(id){ timers.push(id); return id; }
-
- function setNeedleAngle(angle){
- const p = needle();
- if(!p) return;
- p.style.setProperty('animation', 'none', 'important');
- p.style.setProperty('transition', 'none', 'important');
- p.style.setProperty('transform-origin', '50% 100%', 'important');
- p.style.setProperty('transform', `translateX(-50%) rotate(${angle.toFixed(3)}deg)`, 'important');
- }
-
- function setNeedleLeft(){ setNeedleAngle(-MAX_ANGLE); }
-
- function clearMetroSync(){
- serial += 1;
- if(rafId){ cancelAnimationFrame(rafId); rafId = null; }
- timers.forEach(id => clearTimeout(id));
- timers = [];
- if(cleanMetroTimer){ clearTimeout(cleanMetroTimer); clearInterval(cleanMetroTimer); cleanMetroTimer = null; }
- if(metroTimer){ clearTimeout(metroTimer); clearInterval(metroTimer); metroTimer = null; }
- if(typeof metroEndpointTimer !== 'undefined' && metroEndpointTimer){ clearTimeout(metroEndpointTimer); metroEndpointTimer = null; }
- if(typeof metroEndpointLoopTimer !== 'undefined' && metroEndpointLoopTimer){ clearTimeout(metroEndpointLoopTimer); metroEndpointLoopTimer = null; }
- if(typeof metroPendulumFrame !== 'undefined' && metroPendulumFrame){ cancelAnimationFrame(metroPendulumFrame); metroPendulumFrame = null; }
- if(typeof metroRafId !== 'undefined' && metroRafId){ cancelAnimationFrame(metroRafId); metroRafId = null; }
- }
-
- function scheduleAudio(accented, endpointDelayMs){
- ensureAudio();
- const startWhen = audioCtx.currentTime + Math.max(0, endpointDelayMs - CLICK_DUR_MS / 2) / 1000;
- const osc = audioCtx.createOscillator();
- const gain = audioCtx.createGain();
- osc.type = 'square';
- osc.frequency.setValueAtTime(accented ? 1300 : 820, startWhen);
- gain.gain.setValueAtTime(0.0001, startWhen);
- gain.gain.exponentialRampToValueAtTime(accented ? 0.22 : 0.13, startWhen + 0.006);
- gain.gain.exponentialRampToValueAtTime(0.0001, startWhen + CLICK_DUR_MS / 1000);
- osc.connect(gain).connect(audioCtx.destination);
- osc.start(startWhen);
- osc.stop(startWhen + (CLICK_DUR_MS + 8) / 1000);
- }
-
- function applyBeatUi(accented){
- if(!metroRunning) return;
- renderBeats();
- const v = visual();
- if(v){
- v.classList.remove('beat-hit', 'accent-hit');
- if(accented){
- void v.offsetWidth;
- v.classList.add('accent-hit');
- addTimer(setTimeout(() => v.classList.remove('accent-hit'), ACCENT_FLASH_MS));
- }
- }
- beatIndex = (beatIndex + 1) % meter;
- }
-
- function scheduleEndpoint(endpoint, endpointDelayMs){
- const accented = accentMode === 'all' || (accentMode === 'first' && beatIndex === 0);
- scheduleAudio(accented, endpointDelayMs);
-
- const dotStart = Math.max(0, endpointDelayMs - DOT_TRANSITION_MS / 2);
- addTimer(setTimeout(() => applyBeatUi(accented), dotStart));
- }
-
- function frame(mySerial, now){
- if(!metroRunning || mySerial !== serial) return;
- const elapsed = Math.max(0, now - startTs);
- setNeedleAngle(angleAtElapsed(elapsed));
-
- const endpointDelay = endpointTimeMs(nextEndpoint) - elapsed;
- while(endpointDelay <= SCHED_LOOKAHEAD_MS){
- if(nextEndpoint >= 1){
- scheduleEndpoint(nextEndpoint, endpointTimeMs(nextEndpoint) - elapsed);
- }
- nextEndpoint += 1;
- if(endpointTimeMs(nextEndpoint) - elapsed > SCHED_LOOKAHEAD_MS) break;
- }
-
- rafId = requestAnimationFrame(t => frame(mySerial, t));
- }
-
- startMetronome = function(){
- ensureAudio();
- clearMetroSync();
- metroRunning = true;
- beatIndex = 0;
- nextEndpoint = 1;
- const mySerial = serial;
- const v = visual();
- if(v){
- v.classList.add('running');
- v.classList.remove('beat-hit', 'accent-hit');
- v.style.setProperty('--metro-duration', `${beatMs()}ms`);
- }
- setNeedleLeft();
- renderBeats();
- syncCleanMetronomeUi();
- requestAnimationFrame(t => {
- if(!metroRunning || mySerial !== serial) return;
- startTs = t;
- setNeedleLeft();
- rafId = requestAnimationFrame(tt => frame(mySerial, tt));
- });
- };
-
- stopMetronome = function(){
- metroRunning = false;
- clearMetroSync();
- beatIndex = 0;
- const v = visual();
- if(v) v.classList.remove('running', 'beat-hit', 'accent-hit');
- setNeedleLeft();
- syncCleanMetronomeUi();
- renderBeats();
- };
-
- startPendulumMotion = setNeedleLeft;
- stopPendulumMotion = function(){ clearMetroSync(); setNeedleLeft(); };
- tick = function(){
- const accented = accentMode === 'all' || (accentMode === 'first' && beatIndex === 0);
- scheduleAudio(accented, CLICK_DUR_MS / 2);
- applyBeatUi(accented);
- };
-
- const prevInit = initMetronome;
- initMetronome = function(){
- prevInit();
- if(!metroRunning) requestAnimationFrame(setNeedleLeft);
- };
-})();
-
-/* EasyTune v55 — BPM display button opens Tempo modal */
-(function(){
- var prevInit = initMetronome;
- initMetronome = function(){
- prevInit();
- document.querySelector('#metroCleanBpmButton')?.addEventListener('click', function(){
- openMetroSettings('metroTempoModal');
- });
- };
-})();
+loadManifestVersion().then(() => { showApp(); syncInstallButtons(); });
diff --git a/manifest.webmanifest b/manifest.webmanifest
index ef16f2b..27fd378 100644
--- a/manifest.webmanifest
+++ b/manifest.webmanifest
@@ -2,7 +2,7 @@
"name": "EasyTune",
"short_name": "EasyTune",
"description": "Transpozycja, akordy i metronom dla gitarzystów.",
- "version": "1.2",
+ "version": "1.3",
"id": "./",
"start_url": "./",
"scope": "./",
diff --git a/styles.css b/styles.css
index 4f6600d..bb8c88a 100644
--- a/styles.css
+++ b/styles.css
@@ -4690,14 +4690,3 @@ label[for="chordSearch"]{
.metro-clean-visual.accent-hit::before{
animation:metroAccentFlash .14s ease-out 1 !important;
}
-
-/* EasyTune v55 — fix beats container full width + BPM button cursor */
-/* .metro-clean-beats has no explicit width; parent grid's justify-items:center
- (set in v39) causes it to shrink-wrap to ~132px. Force full width. */
-.metro-clean-beats {
- width: 100% !important;
-}
-/* BPM display button now opens Tempo modal — restore pointer cursor */
-#metroCleanBpmButton {
- cursor: pointer !important;
-}