levineuwirth.org/static/js/score-reader.js

136 lines
5.2 KiB
JavaScript

/* score-reader.js — Page-turn navigation for the full score reader.
Configuration is read from #score-reader-stage data attributes:
data-page-count — total number of SVG pages
data-pages — comma-separated list of absolute page image URLs
*/
(function () {
'use strict';
var stage = document.getElementById('score-reader-stage');
var img = document.getElementById('score-page-img');
var counter = document.getElementById('score-page-counter');
var prevBtn = document.getElementById('score-prev');
var nextBtn = document.getElementById('score-next');
if (!stage || !img || !counter || !prevBtn || !nextBtn) return;
var rawPages = stage.dataset.pages || '';
var pages = rawPages.split(',').filter(function (p) { return p.length > 0; });
var pageCount = pages.length;
var currentPage = 1;
if (pageCount === 0) return; /* nothing to display */
/* Read ?p= from the query string for deep linking. */
var qs = new URLSearchParams(window.location.search);
var initPage = parseInt(qs.get('p'), 10);
if (!isNaN(initPage) && initPage >= 1 && initPage <= pageCount) {
currentPage = initPage;
}
/* ------------------------------------------------------------------
Navigation
------------------------------------------------------------------ */
function navigate(page) {
if (page < 1 || page > pageCount) return;
currentPage = page;
img.src = pages[currentPage - 1];
img.alt = 'Score page ' + currentPage;
counter.textContent = 'p. ' + currentPage + ' / ' + pageCount;
prevBtn.disabled = (currentPage === 1);
nextBtn.disabled = (currentPage === pageCount);
updateActiveMovement();
/* Replace URL so the page is bookmarkable at the current position.
The back button still returns to the landing page. */
history.replaceState(null, '', '?p=' + currentPage);
/* Preload the adjacent pages for smooth turning. */
if (currentPage > 1) new Image().src = pages[currentPage - 2];
if (currentPage < pageCount) new Image().src = pages[currentPage];
}
/* ------------------------------------------------------------------
Movement buttons — highlight the movement that contains currentPage
------------------------------------------------------------------ */
var mvtButtons = Array.from(document.querySelectorAll('.score-reader-mvt'));
function updateActiveMovement() {
/* Find the last movement whose start page ≤ currentPage. */
var active = null;
mvtButtons.forEach(function (btn) {
var p = parseInt(btn.dataset.page, 10);
if (!isNaN(p) && p <= currentPage) active = btn;
});
mvtButtons.forEach(function (btn) {
btn.classList.toggle('is-active', btn === active);
});
}
mvtButtons.forEach(function (btn) {
btn.addEventListener('click', function () {
var p = parseInt(btn.dataset.page, 10);
if (!isNaN(p)) navigate(p);
});
});
/* ------------------------------------------------------------------
Prev / next buttons
------------------------------------------------------------------ */
prevBtn.addEventListener('click', function () { navigate(currentPage - 1); });
nextBtn.addEventListener('click', function () { navigate(currentPage + 1); });
/* ------------------------------------------------------------------
Keyboard
------------------------------------------------------------------ */
document.addEventListener('keydown', function (e) {
/* Ignore keypresses while focus is in the settings panel. */
var panel = document.querySelector('.settings-panel');
if (panel && panel.classList.contains('is-open')) return;
if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
navigate(currentPage + 1);
e.preventDefault();
} else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
navigate(currentPage - 1);
e.preventDefault();
} else if (e.key === 'Escape') {
history.back();
}
});
/* ------------------------------------------------------------------
Touch swipe — left/right swipe to turn pages
Threshold: ≥50px horizontal, <30px vertical drift
------------------------------------------------------------------ */
var touchStartX = 0;
var touchStartY = 0;
stage.addEventListener('touchstart', function (e) {
touchStartX = e.changedTouches[0].clientX;
touchStartY = e.changedTouches[0].clientY;
}, { passive: true });
stage.addEventListener('touchend', function (e) {
var dx = e.changedTouches[0].clientX - touchStartX;
var dy = e.changedTouches[0].clientY - touchStartY;
if (Math.abs(dx) < 50 || Math.abs(dy) > 30) return;
if (dx < 0) navigate(currentPage + 1);
else navigate(currentPage - 1);
}, { passive: true });
/* ------------------------------------------------------------------
Init — load the starting page
------------------------------------------------------------------ */
navigate(currentPage);
}());