PDF, JPG, PNG, TIFF o BMP con notazione quadrata su tetragramma. L'app riconosce rigo, chiavi, neumi e testo; tu correggi sull'immagine e scarichi la versione su pentagramma. Puoi caricare anche un file GABC (Gregorio, GregoBase).
Non hai una partitura sotto mano? : il Salve Regina in tono semplice.
Nessuna selezioneClic su una nota, una sillaba o un rigo
note riconosciute, sillabe (in corsivo quelle incerte), divisiones, custos (non trascritto). Trascina una nota in su o in giù, oppure usa ↑↓. ←→ per scorrere gli elementi.
` : `
Carica un file per iniziare.
`;
return;
}
const { st, it } = f;
const H = (html) => { const d = document.createElement('div'); d.innerHTML = html; while (d.firstChild) box.appendChild(d.firstChild); };
const on = (id, evn, fn) => { const e = $(id); if (e) e.addEventListener(evn, fn); };
const clefIdx = p.staves.indexOf(st);
// Euouae: scelta tra trascriverlo così com'è o inserire il Gloria Patri completo
const euPanel = (eu) => {
const k = String(st.eu.indexOf(eu));
const vs = GR.EU.variants(), cur = eu.tone + '.' + eu.ending;
const unsure = eu.dist > Math.max(1, Math.round(eu.ids.length * 0.2));
H(`
Euouae${unsure ? 'tono incerto: controlla' : 'differentia del tono salmodico'}
Sillabe del rigoseparate da spazi; "-" a fine sillaba se la parola continua
Le sillabe vengono riassegnate alle note già con sillaba, nello stesso ordine. Se ne scrivi di più o di meno, l'app le riallinea alle posizioni dei neumi.
`;
$('iAll').value = notes.map(n => n.syl.text + (n.syl.hyph ? '-' : '')).join(' ');
$('iAll').focus();
$('iAllNo').onclick = () => renderInspector();
$('iAllOk').onclick = () => {
const toks = $('iAll').value.trim().split(/\s+/).filter(Boolean).map(t => ({ text: t.replace(/-+$/, ''), hyph: /-$/.test(t) }));
snapshot();
if (toks.length === notes.length) notes.forEach((n, i) => { n.syl = { text: toks[i].text, hyph: toks[i].hyph }; });
else {
const all = st.items.filter(i => i.kind === 'note');
const anchors = notes.map(n => n.x);
const syl = toks.map((t, i) => ({ text: t.text, hyph: t.hyph, ax: anchors.length ? anchors[Math.min(anchors.length - 1, Math.round(i * (anchors.length - 1) / Math.max(1, toks.length - 1)))] : all[0].x }));
M.applySyllables(st, syl);
}
afterChange();
};
}
// ---------- tastiera ----------
document.addEventListener('keydown', ev => {
const tag = (ev.target.tagName || '').toLowerCase();
const typing = tag === 'input' || tag === 'select' || tag === 'textarea';
const mod = ev.metaKey || ev.ctrlKey;
if (mod && ev.key.toLowerCase() === 'z' && !typing) { ev.preventDefault(); ev.shiftKey ? restore(S.redo, S.undo) : restore(S.undo, S.redo); return; }
if (mod && ev.key.toLowerCase() === 'o') { ev.preventDefault(); $('file').click(); return; }
if (typing || mod) return;
const f = S.sel && findItem(S.sel);
const k = ev.key;
if (k === 'Escape') { if (!$('sheet').hidden) { $('sheet').hidden = true; return; } setMode('select'); S.sel = null; drawOverlay(); renderInspector(); return; }
if (k === 'v' || k === 'V') return setMode('select');
if (k === 'n' || k === 'N') return setMode('note');
if (k === 'b' || k === 'B') return setMode('bar');
if (k === 'f' || k === 'F') return setMode('flat');
if (k === 'c' || k === 'C') return setMode('clef');
if (k === 'ArrowRight' || k === 'Tab' && !ev.shiftKey) { ev.preventDefault(); return moveSel(1); }
if (k === 'ArrowLeft' || k === 'Tab' && ev.shiftKey) { ev.preventDefault(); return moveSel(-1); }
if (!f || !f.it) return;
const it = f.it;
if ((k === 'ArrowUp' || k === 'ArrowDown') && it.step != null) { ev.preventDefault(); edit(x => x.step += k === 'ArrowUp' ? 1 : -1); return; }
if ((k === 'ArrowUp' || k === 'ArrowDown') && it.line != null) { ev.preventDefault(); edit(x => x.line = Math.max(1, Math.min(4, x.line + (k === 'ArrowUp' ? 1 : -1)))); return; }
if (k === 'Backspace' || k === 'Delete') { ev.preventDefault(); return deleteSel(); }
if (it.kind === 'note') {
if (k === 'm' || k === 'M') return edit(x => x.mora = !x.mora);
if (k === 'e' || k === 'E') return edit(x => x.episema = !x.episema);
if (k === 'l' || k === 'L') return toggleLink();
if (k === 't' || k === 'T' || k === 'Enter') { ev.preventDefault(); const i = $('iSyl'); if (i) { i.focus(); i.select(); } }
}
});
// ---------- anteprima moderna ----------
let measCtx = null;
function measure(t, size) { if (!measCtx) measCtx = document.createElement('canvas').getContext('2d'); measCtx.font = size + 'px ' + RD.FONT; return measCtx.measureText(t).width; }
// carattere del testo (EB Garamond, incorporato): per anteprima/PNG come FontFace, per il PDF dentro jsPDF
const TEXT_FONTS = [['font-reg', 'normal', '400', 'EBGaramond-Regular.ttf'], ['font-ita', 'italic', '400', 'EBGaramond-Italic.ttf'], ['font-bold', 'normal', '700', 'EBGaramond-Bold.ttf']];
const fontB64 = id => { const e = document.getElementById(id); return e ? e.textContent.trim() : ''; };
const textFontsReady = (async () => {
try {
await Promise.all(TEXT_FONTS.map(async ([id, style, weight]) => {
const b = atob(fontB64(id)), u = new Uint8Array(b.length); for (let i = 0; i < b.length; i++) u[i] = b.charCodeAt(i);
const f = new FontFace('PG Garamond', u.buffer, { style: style, weight: weight }); await f.load(); document.fonts.add(f);
}));
} catch (e) { console.warn('Carattere del testo non caricato', e); }
measCtx = null; if (typeof scheduleRender === 'function') scheduleRender();
})();
function addPdfFonts(doc) {
try {
TEXT_FONTS.forEach(([id, style, weight, file]) => { const b = fontB64(id); if (!b) return; doc.addFileToVFS(file, b); doc.addFont(file, RD.PDF_FONT, weight === '700' ? 'bold' : style); });
} catch (e) { console.warn('Carattere del testo non incorporato nel PDF', e); }
}
function buildSeq() {
return M.buildSequence(GR.EU.expandDoc({ pages: S.pages.filter(p => p.staves) }), { clef: S.settings.clef, octave: 'auto', octaveAdj: S.settings.octaveAdj, slurs: S.settings.slurs, value: S.settings.value, episema: S.settings.episema || 'long', transpose: S.settings.transpose === 'auto' ? 'auto' : (+S.settings.transpose || 0), slurPos: S.settings.slurPos || 'below' });
}
// ---------- piè di pagina: logo e nome del sito ----------
// Sul sito (plugin) arrivano da WordPress; nella versione Mac dalle impostazioni.
const SITE = window.PEREGRINUS_SITE || null;
const IMGS = {};
function footerCfg() {
if (SITE) return SITE.footer === false ? null : { name: SITE.name || '', url: SITE.url || '', logo: IMGS.logo ? { key: 'logo', aspect: IMGS.logo.aspect } : null };
if (!S.settings.foot) return null;
return { name: S.settings.footName || '', url: S.settings.footUrl || '', logo: IMGS.logo ? { key: 'logo', aspect: IMGS.logo.aspect } : null };
}
async function loadFooterLogo() {
delete IMGS.logo;
const src = SITE ? SITE.logo : S.settings.footLogo;
if (!src) { scheduleRender(); return; }
try {
const img = await new Promise((res, rej) => { const i = new Image(); i.onload = () => res(i); i.onerror = () => rej(new Error('logo')); i.src = src; });
const w = img.naturalWidth || 300, h = img.naturalHeight || 100;
// versione PNG ad alta risoluzione per il PDF (funziona anche con loghi SVG)
const k = Math.min(4, 600 / Math.max(w, h)) || 1, c = document.createElement('canvas');
c.width = Math.max(1, Math.round(w * k)); c.height = Math.max(1, Math.round(h * k));
c.getContext('2d').drawImage(img, 0, 0, c.width, c.height);
IMGS.logo = { img: c, png: c.toDataURL('image/png'), aspect: w / h };
} catch (e) { console.warn('Logo del piè di pagina non caricato', e); }
scheduleRender();
}
function pieceMode() { const p = S.pages.find(p => p.mode); return p ? p.mode : ''; }
// dimensione del testo: passi di mezzo punto, da −2 a +3 pt rispetto al predefinito
// dimensione del testo: punti interi da 10 a 14, predefinito 12
const TXT_MIN = 10, TXT_MAX = 14;
const textPt = () => Math.max(TXT_MIN, Math.min(TXT_MAX, Math.round(+S.settings.textPt || RD.TEXT_PT)));
function syncTextSize() { const el = $('txtSize'); if (!el) return; const v = textPt(); el.textContent = v + ' pt'; $('txtMinus').disabled = v <= TXT_MIN; $('txtPlus').disabled = v >= TXT_MAX; if ($('setTextSize')) $('setTextSize').value = String(v); }
function setTextPt(v) { S.settings.textPt = Math.max(TXT_MIN, Math.min(TXT_MAX, Math.round(v))); saveSettings(); syncTextSize(); scheduleRender(); }
function layoutOpts() { const md = pieceMode(); return { title: S.settings.title, subtitle: md ? T('Modo {0}', md) : '', footer: footerCfg(), font: textPt() / RD.defaults.s }; }
let renderT = null;
function scheduleRender() { clearTimeout(renderT); renderT = setTimeout(renderOut, 120); }
function renderOut() {
const host = $('outPages'); host.innerHTML = '';
const ready = S.pages.filter(p => p.staves);
if (!ready.length) { host.innerHTML = '
La trascrizione apparirà qui
Note su pentagramma in chiave di violino, con testo e legature sui melismi. Ogni correzione fatta a sinistra si aggiorna subito.
'; $('outInfo').textContent = ''; return; }
const seq = buildSeq(); const kChanged = !S.lastSeq || S.lastSeq.keyIn !== seq.keyIn || (seq.transposeAuto && S.lastSeq.transpose !== seq.transpose); S.lastSeq = seq; if (kChanged) fillTranspose();
if (PL.on) playStop();
const L = RD.layout(seq, layoutOpts(), measure); S.layout = L; S.outPages = [];
const avail = Math.max(200, $('outView').clientWidth - 28);
const dpr = window.devicePixelRatio || 1;
const cssW = Math.min(avail, 820), k = cssW / L.opt.pageW;
L.pages.forEach((pg, pi) => {
const c = document.createElement('canvas');
c.width = Math.round(L.opt.pageW * k * dpr); c.height = Math.round(L.opt.pageH * k * dpr);
c.style.width = cssW + 'px';
const ctx = c.getContext('2d'); ctx.fillStyle = '#fff'; ctx.fillRect(0, 0, c.width, c.height);
RD.drawCanvas(ctx, pg, k * dpr, '#111', '#fff', IMGS);
// evidenzia la nota selezionata
if (S.sel) { const h = pg.hits.find(h => h.ref === S.sel.id); if (h) { ctx.save(); ctx.scale(k * dpr, k * dpr); ctx.strokeStyle = '#B5820A'; ctx.lineWidth = 1.2; ctx.strokeRect(h.x0 - 2, h.y0 - 2, h.x1 - h.x0 + 4, h.y1 - h.y0 + 4); ctx.restore(); } }
c.onclick = ev => {
const r = c.getBoundingClientRect(), x = (ev.clientX - r.left) / k, y = (ev.clientY - r.top) / k;
const h = pg.hits.find(h => x >= h.x0 - 3 && x <= h.x1 + 3 && y >= h.y0 - 3 && y <= h.y1 + 3);
if (!h) return;
for (let i = 0; i < S.pages.length; i++) for (let si = 0; si < (S.pages[i].staves || []).length; si++) if (S.pages[i].staves[si].items.some(it => it.id === h.ref)) { if (i !== S.cur) showPage(i); select(si, h.ref); scrollToSel(); return; }
};
const wrap = document.createElement('div'); wrap.className = 'opage'; wrap.appendChild(c);
host.appendChild(wrap); S.outPages.push({ wrap, k });
});
const nn = seq.events.filter(e => e.type === 'note').length;
$('outInfo').textContent = L.pages.length === 1 ? T('1 pagina A4 · {0} note', nn) : T('{0} pagine A4 · {1} note', L.pages.length, nn);
if (S.sel) renderInspectorPitchOnly();
}
function renderInspectorPitchOnly() { const f = S.sel && findItem(S.sel); if (f && f.it && f.it.kind === 'note' && document.activeElement && document.activeElement.id !== 'iSyl') renderInspector(); }
// ---------- ascolto ----------
// Sintesi con Web Audio, senza file esterni (funziona anche offline). Si sente quello che è scritto:
// trasposizione e ottava comprese (la chiave di violino ottavata suona un'ottava sotto).
// Valori: croma = 1 battito alla velocità scelta; le stanghette aggiungono un respiro.
const BAR_REST = { minima: 0.25, minor: 1, maior: 2, finalis: 3 }; // in crome (minima: appena un respiro)
function playSchedule(seq, fromRef) {
const out = []; let t = 0, started = !fromRef;
const oct = seq.clef === 'treble8' ? -12 : 0;
seq.events.forEach(e => {
if (!started && e.type === 'note' && (e.ref === fromRef || (e.refs || []).includes(fromRef))) started = true;
if (!started) return;
if (e.type === 'bar') { if (out.length) t += BAR_REST[e.bar] != null ? BAR_REST[e.bar] : 1; return; }
if (e.type !== 'note') return;
const dur = seq.rhythmic ? (e.dur || 1) : 1;
out.push({ t, dur, midi: M.semis(e.d, e.alter || 0) + 12 + oct, ref: e.ref, refs: e.refs || [e.ref] });
t += dur;
});
return out;
}
function voiceNote(ctx, dest, f, t, len, kind) {
const g = ctx.createGain(); g.connect(dest);
const a = Math.min(0.06, len * 0.3), r = Math.min(0.09, len * 0.35), end = t + len;
g.gain.setValueAtTime(0, t); g.gain.linearRampToValueAtTime(1, t + a); g.gain.setValueAtTime(1, Math.max(t + a, end - r)); g.gain.linearRampToValueAtTime(0, end + 0.02);
const oscs = [];
if (kind === 'voice') {
const o = ctx.createOscillator(); o.type = 'sawtooth'; o.frequency.value = f;
const vib = ctx.createOscillator(), vg = ctx.createGain(); vib.frequency.value = 5.2; vg.gain.value = f * 0.004; vib.connect(vg); vg.connect(o.frequency);
const lp = ctx.createBiquadFilter(); lp.type = 'lowpass'; lp.frequency.value = 2600; o.connect(lp);
[[800, 6, 1], [1150, 8, 0.6], [2900, 10, 0.12]].forEach(([fq, q, lv]) => { const bp = ctx.createBiquadFilter(); bp.type = 'bandpass'; bp.frequency.value = fq; bp.Q.value = q; const bg = ctx.createGain(); bg.gain.value = lv * 3.2; lp.connect(bp); bp.connect(bg); bg.connect(g); });
oscs.push(o, vib);
} else {
[[1, 0.6], [2, 0.22], [3, 0.08], [4, 0.04]].forEach(([h, lv]) => { const o = ctx.createOscillator(); o.type = 'sine'; o.frequency.value = f * h; const og = ctx.createGain(); og.gain.value = lv; o.connect(og); og.connect(g); oscs.push(o); });
}
oscs.forEach(o => { o.start(t); o.stop(end + 0.05); });
}
function playMarkTo(ref) {
const L = S.layout; if (!L || !S.outPages) return;
for (let pi = 0; pi < L.pages.length; pi++) {
const h = L.pages[pi].hits.find(h => h.ref === ref); if (!h || !S.outPages[pi]) continue;
const { wrap, k } = S.outPages[pi];
if (!PL.mark || PL.mark.parentNode !== wrap) { if (PL.mark) PL.mark.remove(); PL.mark = document.createElement('div'); PL.mark.className = 'playmark'; wrap.appendChild(PL.mark); }
Object.assign(PL.mark.style, { left: (h.x0 - 2) * k + 'px', top: (h.y0 - 2) * k + 'px', width: (h.x1 - h.x0 + 4) * k + 'px', height: (h.y1 - h.y0 + 4) * k + 'px' });
// tieni la nota in vista
const vw = $('outView'), r = PL.mark.getBoundingClientRect(), rv = vw.getBoundingClientRect();
if (r.top < rv.top + 40 || r.bottom > rv.bottom - 40) vw.scrollTop += r.top - rv.top - rv.height * 0.35;
return;
}
}
// cursore anche sull'originale (pannello di sinistra): riquadro attorno alle note gregoriane che suonano
// (più d'una quando una nota del pentagramma riunisce note ribattute); cambia pagina e scorre da solo
function playMarkSrc(refs) {
const pi = PL.pageOf[refs[0]]; if (pi == null) return;
if (pi !== S.cur) showPage(pi);
const svg = $('overlay'); if (!svg) return;
let box = null;
refs.forEach(r => { const n = svg.querySelector(`[data-id="${r}"]`); if (!n || !n.getBBox) return; const b = n.getBBox(); box = box ? { x0: Math.min(box.x0, b.x), y0: Math.min(box.y0, b.y), x1: Math.max(box.x1, b.x + b.width), y1: Math.max(box.y1, b.y + b.height) } : { x0: b.x, y0: b.y, x1: b.x + b.width, y1: b.y + b.height }; });
if (!box) return;
if (!PL.srcMark || PL.srcMark.parentNode !== svg) { PL.srcMark = document.createElementNS(SVGNS, 'rect'); PL.srcMark.setAttribute('class', 'srcplay'); svg.appendChild(PL.srcMark); }
const p = S.pages[S.cur], pad = (p && p.space ? p.space : 20) * 0.25;
['x', 'y', 'width', 'height'].forEach((k, j) => PL.srcMark.setAttribute(k, [box.x0 - pad, box.y0 - pad, box.x1 - box.x0 + 2 * pad, box.y1 - box.y0 + 2 * pad][j]));
PL.srcMark.setAttribute('rx', pad);
const vw = $('srcView'), r = PL.srcMark.getBoundingClientRect(), rv = vw.getBoundingClientRect();
if (r.top < rv.top + 30 || r.bottom > rv.bottom - 30) vw.scrollTop += r.top - rv.top - rv.height * 0.35;
if (r.left < rv.left + 20 || r.right > rv.right - 20) vw.scrollLeft += r.left - rv.left - rv.width * 0.3;
}
function playStart() {
if (!needData()) return;
const seq = S.lastSeq || buildSeq();
const fromRef = S.sel && S.sel.id;
let notes = playSchedule(seq, fromRef);
if (!notes.length && fromRef) notes = playSchedule(seq, null);
if (!notes.length) return;
const AC = window.AudioContext || window.webkitAudioContext;
if (!AC) { msg('Questo browser non può riprodurre l\u2019audio.'); return; }
const ctx = new AC(), master = ctx.createGain(), comp = ctx.createDynamicsCompressor();
master.gain.value = 0.22; master.connect(comp); comp.connect(ctx.destination);
const beat = 60 / (+S.settings.playTempo || 180), t0 = ctx.currentTime + 0.12, kind = S.settings.playSound || 'organ';
notes.forEach(n => { n.at = t0 + n.t * beat; n.len = n.dur * beat; voiceNote(ctx, master, 440 * Math.pow(2, (n.midi - 69) / 12), n.at, n.len * 0.97, kind); });
const pageOf = {}; S.pages.forEach((pg, pi) => (pg.staves || []).forEach(st => st.items.forEach(it => { pageOf[it.id] = pi; })));
Object.assign(PL, { on: true, ctx, master, notes, t0, pageOf });
$('btnPlay').textContent = '■ Ferma'; $('btnPlay').classList.add('on');
let cur = -1;
const tick = () => {
if (!PL.on) return;
const now = ctx.currentTime;
let i = cur; while (i + 1 < notes.length && notes[i + 1].at <= now) i++;
if (i !== cur && i >= 0) { cur = i; playMarkTo(notes[i].ref); playMarkSrc(notes[i].refs); }
const last = notes[notes.length - 1];
if (now > last.at + last.len + 0.2) { playStop(); return; }
PL.raf = requestAnimationFrame(tick);
};
PL.raf = requestAnimationFrame(tick);
}
function playStop() {
PL.on = false; cancelAnimationFrame(PL.raf);
if (PL.ctx) { try { PL.master.gain.setTargetAtTime(0, PL.ctx.currentTime, 0.02); } catch (e) {} const c = PL.ctx; setTimeout(() => c.close().catch(() => {}), 150); }
PL.ctx = null; if (PL.mark) { PL.mark.remove(); PL.mark = null; } if (PL.srcMark) { PL.srcMark.remove(); PL.srcMark = null; }
$('btnPlay').textContent = '▶ Ascolta'; $('btnPlay').classList.remove('on');
}
$('btnPlay').onclick = () => (PL.on ? playStop() : playStart());
$('setEpisema').onchange = () => { S.settings.episema = $('setEpisema').value; saveSettings(); scheduleRender(); };
$('setTempo').onchange = () => { S.settings.playTempo = +$('setTempo').value; saveSettings(); };
$('setSound').onchange = () => { S.settings.playSound = $('setSound').value; saveSettings(); };
// ---------- esportazione ----------
function download(blob, name) {
const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = name;
document.body.appendChild(a); a.click(); a.remove();
setTimeout(() => URL.revokeObjectURL(a.href), 4000);
}
function needData() { if (!S.pages.some(p => p.staves && p.staves.length)) { msg('Carica prima una partitura.'); return false; } return true; }
function base() { return (S.settings.title || S.fileBase || 'gregoriano').replace(/[\\/:*?"<>|]+/g, '_').trim() || 'gregoriano'; }
$('expXml').onclick = () => {
if (!needData()) return;
const seq = buildSeq();
const xml = M.toMusicXML(seq, { title: S.settings.title || T('Canto gregoriano'), value: S.settings.value, clef: S.settings.clef, mode: pieceMode(), textPt: textPt() });
download(new Blob([xml], { type: 'application/vnd.recordare.musicxml+xml' }), base() + '.musicxml');
msg(T('MusicXML salvato: {0}', base() + '.musicxml'));
track('esportazione', { formato: 'musicxml', titolo: S.settings.title || '', incipit: incipit() });
};
$('expPdf').onclick = () => {
if (!needData()) return;
try {
const L = RD.layout(buildSeq(), layoutOpts(), measure);
const doc = new jspdf.jsPDF({ unit: 'pt', format: 'a4', compress: true });
addPdfFonts(doc);
doc.setProperties({ title: S.settings.title || T('Canto gregoriano'), creator: 'Peregrinus' });
L.pages.forEach((pg, i) => { if (i) doc.addPage('a4'); RD.drawPdf(doc, pg, IMGS); });
download(doc.output('blob'), base() + '.pdf');
msg(T('PDF salvato: {0}', base() + '.pdf'));
track('esportazione', { formato: 'pdf', titolo: S.settings.title || '', incipit: incipit() });
} catch (e) { errMsg(e); }
};
$('expPng').onclick = async () => {
if (!needData()) return;
const L = RD.layout(buildSeq(), layoutOpts(), measure);
const k = 300 / 72;
for (let i = 0; i < L.pages.length; i++) {
const c = document.createElement('canvas'); c.width = Math.round(L.opt.pageW * k); c.height = Math.round(L.opt.pageH * k);
const ctx = c.getContext('2d'); ctx.fillStyle = '#fff'; ctx.fillRect(0, 0, c.width, c.height);
RD.drawCanvas(ctx, L.pages[i], k, '#000', '#fff', IMGS);
const blob = await new Promise(r => c.toBlob(r, 'image/png'));
download(blob, base() + (L.pages.length > 1 ? '-p' + (i + 1) : '') + '.png');
if (L.pages.length > 1) await new Promise(r => setTimeout(r, 450));
}
track('esportazione', { formato: 'png', titolo: S.settings.title || '', incipit: incipit() });
msg(L.pages.length > 1 ? T('PNG salvati a 300 dpi ({0} pagine).', L.pages.length) : T('PNG salvato a 300 dpi (1 pagina).'));
};
// ---------- impostazioni ----------
function syncSettingsUI() {
const s = S.settings;
$('footerSettings').hidden = !!SITE; $('footerSite').hidden = !SITE;
$('setFoot').checked = s.foot !== false; $('setFootName').value = s.footName || ''; $('setFootUrl').value = s.footUrl || '';
const pv = $('footLogoPrev'); if (s.footLogo) { pv.src = s.footLogo; pv.style.display = 'block'; } else { pv.removeAttribute('src'); pv.style.display = 'none'; }
$('setTitle').value = s.title || ''; $('setClef').value = s.clef; $('setOct').value = String(s.octaveAdj); $('setValue').value = s.value; $('setSlurs').value = s.slurs;
$('setOcr').checked = !!s.ocr; $('setRed').checked = !!s.ignoreRed; $('setAbove').value = String(s.above); $('setDpi').value = String(s.dpi); $('setEuouae').value = s.euouae || 'keep'; syncTextSize(); $('setSlurPos').value = s.slurPos || 'below'; fillTranspose(); $('setTempo').value = String(s.playTempo || 180); $('setEpisema').value = s.episema || 'long'; $('setSound').value = s.playSound || 'organ';
}
$('btnSettings').onclick = () => { syncSettingsUI(); $('sheet').hidden = false; };
$('sheetClose').onclick = () => { $('sheet').hidden = true; };
[['setTitle', 'title', v => v], ['setClef', 'clef', v => v], ['setOct', 'octaveAdj', v => +v], ['setValue', 'value', v => v], ['setSlurs', 'slurs', v => v], ['setOcr', 'ocr', null], ['setRed', 'ignoreRed', null], ['setAbove', 'above', v => +v], ['setDpi', 'dpi', v => +v], ['setFoot', 'foot', null], ['setFootName', 'footName', v => v], ['setFootUrl', 'footUrl', v => v], ['setEuouae', 'euouae', v => v]].forEach(([id, key, conv]) => {
const e = $(id);
e.addEventListener(e.type === 'text' ? 'input' : 'change', () => { S.settings[key] = conv ? conv(e.value) : e.checked; saveSettings(); scheduleRender(); });
});
let reConfirm = false;
$('btnReanalyze').onclick = async () => {
if (!S.pages.length) return;
if (!reConfirm) { reConfirm = true; $('btnReanalyze').textContent = 'Conferma: le correzioni andranno perse'; $('btnReanalyze').classList.add('primary'); setTimeout(() => { reConfirm = false; $('btnReanalyze').textContent = 'Rianalizza tutte le pagine'; $('btnReanalyze').classList.remove('primary'); }, 5000); return; }
reConfirm = false; $('btnReanalyze').textContent = 'Rianalizza tutte le pagine'; $('btnReanalyze').classList.remove('primary');
if (!S.lastFiles) return;
$('sheet').hidden = true;
await openFiles(S.lastFiles, false);
};
// ---------- apertura, trascinamento, esempio ----------
$('btnOpen').onclick = $('btnOpen2').onclick = () => $('file').click();
$('file').onchange = () => { const fl = Array.from($('file').files); $('file').value = ''; openFiles(fl, false); };
const work = $('work');
['dragenter', 'dragover'].forEach(n => work.addEventListener(n, e => { e.preventDefault(); work.classList.add('drop-hl'); }));
['dragleave', 'drop'].forEach(n => work.addEventListener(n, e => { e.preventDefault(); if (n === 'drop' || e.target === work) work.classList.remove('drop-hl'); }));
work.addEventListener('drop', e => { const fl = Array.from(e.dataTransfer.files || []); if (fl.length) openFiles(fl, e.altKey && S.pages.length > 0); });
async function loadSample() {
// Salve Regina in tono semplice (Liber Usualis 1961, p. 279), PDF di GregoBase
const b64 = document.getElementById('sample-pdf').textContent.trim();
const bin = atob(b64), u = new Uint8Array(bin.length); for (let i = 0; i < bin.length; i++) u[i] = bin.charCodeAt(i);
const f = new File([u], 'Salve Regina (tonus simplex).pdf', { type: 'application/pdf' });
await openFiles([f], false, true);
}
$('btnSample2').onclick = loadSample;
// avviso sui telefoni (schermo stretto, o basso con il tocco): si chiude con «Ho capito» e non torna più
(function () {
const n = $('phoneNote'); if (!n || !window.matchMedia) return;
let ok = false; try { ok = localStorage.getItem('peregrinus.phoneOk') === '1'; } catch (e) {}
if (ok || !matchMedia('(max-width: 759px), (max-height: 500px) and (pointer: coarse)').matches) return;
n.hidden = false;
$('phoneOk').onclick = () => { n.hidden = true; try { localStorage.setItem('peregrinus.phoneOk', '1'); } catch (e) {} };
})();
$('btnFootLogo').onclick = () => $('footLogoFile').click();
$('footLogoFile').onchange = () => {
const f = $('footLogoFile').files[0]; $('footLogoFile').value = ''; if (!f) return;
if (f.size > 1.5e6) { msg('Logo troppo pesante: usa un\'immagine sotto 1,5 MB.'); return; }
const r = new FileReader();
r.onload = () => { S.settings.footLogo = r.result; saveSettings(); syncSettingsUI(); loadFooterLogo(); msg('Logo del piè di pagina impostato.'); };
r.readAsDataURL(f);
};
// trasposizione: da 6 semitoni sotto a 6 sopra; il nome dice dove va il Do dell'originale e l'armatura che ne risulta
const TR_NAMES = { 1: 'un semitono', 2: 'un tono', 3: 'una terza minore', 4: 'una terza maggiore', 5: 'una quarta', 6: 'un tritono' };
function trLabel(t, short) {
if (!t) return short ? T('nessuna') : T('Nessuna (come l\u2019originale)');
const k0 = (S.lastSeq && S.lastSeq.keyIn != null) ? S.lastSeq.keyIn : 0;
const k = k0 + M.interval(t, k0).fifths, arm = k === 0 ? T('senza alterazioni') : Math.abs(k) + (k > 0 ? ' \u266F' : ' \u266D');
const to = I18N.note(M.transposeName(t, k0));
return short ? (t > 0 ? '+' : '') + t + ' (' + to + ')' : T(t > 0 ? '+{0}: {1} sopra — {2} → {3}, armatura {4}' : '−{0}: {1} sotto — {2} → {3}, armatura {4}', Math.abs(t), T(TR_NAMES[Math.abs(t)]), I18N.note('Do'), to, arm);
}
function fillTranspose() {
const v = S.settings.transpose === 'auto' ? 'auto' : String(+S.settings.transpose || 0);
const ta = S.lastSeq && S.lastSeq.transposeAuto ? S.lastSeq.transpose : null;
[['setTranspose', false], ['trSel', true]].forEach(([id, short]) => {
const el = $(id); if (!el) return; el.innerHTML = '';
// automatica: tonalità comoda per coro misto (tra parentesi quella scelta)
const oa = document.createElement('option'); oa.value = 'auto';
oa.textContent = short ? T('auto ({0})', ta == null ? '…' : (ta > 0 ? '+' : '') + ta) : T('Automatica, per coro misto: {0}', ta == null ? '…' : trLabel(ta, true));
el.appendChild(oa);
for (let t = 6; t >= -6; t--) { const o = document.createElement('option'); o.value = String(t); o.textContent = trLabel(t, short); el.appendChild(o); }
el.value = v;
});
}
function setTranspose(v) { S.settings.transpose = v === 'auto' ? 'auto' : Math.max(-6, Math.min(6, Math.round(+v || 0))); saveSettings(); fillTranspose(); scheduleRender(); }
$('setTranspose').onchange = () => setTranspose($('setTranspose').value);
$('trSel').onchange = () => setTranspose($('trSel').value);
$('setSlurPos').onchange = () => { S.settings.slurPos = $('setSlurPos').value; saveSettings(); scheduleRender(); };
fillTranspose();
$('txtMinus').onclick = () => setTextPt(textPt() - 1);
$('txtPlus').onclick = () => setTextPt(textPt() + 1);
$('setTextSize').onchange = () => setTextPt(+$('setTextSize').value);
syncTextSize();
$('btnFootLogoDel').onclick = () => { S.settings.footLogo = ''; saveSettings(); syncSettingsUI(); loadFooterLogo(); };
// avvio
loadFooterLogo();
// sul sito: il marchio riporta alla pagina iniziale (nella lingua della pagina da cui si è arrivati)
if (SITE && SITE.home) {
const b = document.querySelector('.brand');
if (b) { const a = document.createElement('a'); a.className = b.className; a.href = SITE.home; a.target = '_top'; a.style.textDecoration = 'none'; while (b.firstChild) a.appendChild(b.firstChild); b.replaceWith(a); }
}
track('apertura', {});
setMode('select'); renderInspector(); renderOut();
$('btnUndo').disabled = true; $('btnRedo').disabled = true;
})();