Skip to content

Commit 15fe5b4

Browse files
committed
Add page numbers, copyright footer, and jump-to-page navigation
Page mode enhancements: - Page numbers rendered at bottom center of each page - Copyright text drawn in bottom margin of page 1 (replaces DOM footer) - Page nav toolbar visible in all page view modes, not just single-page - Jump-to-page input: type a page number and press Enter to navigate - Scroll-based page tracking updates indicator as user scrolls - Viewport culling for page backgrounds (skip offscreen pages)
1 parent 9477740 commit 15fe5b4

File tree

4 files changed

+133
-7
lines changed

4 files changed

+133
-7
lines changed

index.html

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -338,7 +338,8 @@
338338
</select>
339339
<span id="page_nav" style="display: none;">
340340
<button id="page_prev" title="Previous page" style="font-size: 11px; padding: 2px 6px;">&lt;</button>
341-
<span id="page_indicator" style="font-size: 11px; min-width: 50px; text-align: center; color: var(--text-dim);">1 / 1</span>
341+
<input id="page_input" type="number" min="1" value="1" title="Jump to page" style="width: 32px; text-align: center; font-size: 11px; padding: 1px 2px; -moz-appearance: textfield; border: 1px solid var(--border, #ccc); border-radius: 3px;">
342+
<span id="page_indicator" style="font-size: 11px; color: var(--text-dim);"> / 1</span>
342343
<button id="page_next" title="Next page" style="font-size: 11px; padding: 2px 6px;">&gt;</button>
343344
</span>
344345
</div>

plans/features-todo.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,9 @@ Planned features and improvements, roughly prioritized.
4545
- [x] Companion text fonts — BravuraText, LelandText, PetalumaText, SebastianText loaded via @font-face; all text elements (lyrics, titles, tempo, staff labels) use the matching text font; GoldenAge/Leipzig fall back to serif
4646
- [x] Dynamic markings rendered as SMuFL glyphs — pre-composed glyphs (pp, mp, mf, ff, etc.) from the music font instead of italic text; falls back to individual letter glyph composition for unknown combinations
4747
- [x] Landscape / portrait page orientation toggle — `[Portrait | Landscape]` segmented button group in page mode, swaps page width/height via `getPageDimensions()`, persisted to localStorage
48-
- [ ] Multi-page rendering — display all pages vertically like a PDF viewer (currently only lays out pages but rendering may clip)
49-
- [ ] Page navigation — jump to page N (page indicator + input or prev/next buttons)
50-
- [ ] Page numbers, headers/footers in page mode
48+
- [x] Multi-page rendering — display all pages vertically like a PDF viewer; virtual-scroll architecture (viewport-sized canvas, scroll-based repainting, viewport culling) handles arbitrarily many pages; page backgrounds with drop shadows on gray canvas
49+
- [x] Page navigation — jump to page N via numeric input in toolbar; prev/next buttons; scroll-based page tracking updates indicator in all page view modes (vertical, single-page, two-up, horizontal)
50+
- [x] Page numbers, headers/footers in page mode — page numbers rendered at bottom center of each page; copyright text rendered in bottom margin of page 1
5151
- [ ] Bar compression — fix bug where bars with many notes overflow or don't shrink to fit available width
5252
- [ ] First-system indent for instrument names
5353
- [ ] Melisma/extender lines for slurred notes under one syllable

src/layout/typeset.js

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1867,9 +1867,23 @@ function _drawPageBackgrounds(ctx, pg) {
18671867
var shadowOffset = 4
18681868
var shadowColor = 'rgba(0,0,0,0.25)'
18691869

1870+
// Viewport culling — skip pages entirely outside the visible area.
1871+
var zoom = getZoomLevel()
1872+
var scoreElm = document.getElementById('score')
1873+
var viewTop = (scoreElm?.scrollTop || 0) / zoom
1874+
var viewBottom = viewTop + (scoreElm?.clientHeight || 800) / zoom
1875+
var viewLeft = (scoreElm?.scrollLeft || 0) / zoom
1876+
var viewRight = viewLeft + (scoreElm?.clientWidth || 800) / zoom
1877+
var pad = 50 // extra padding to avoid pop-in
1878+
18701879
for (var p = 0; p < pg.pageCount; p++) {
18711880
var pos = pg.pagePositions[p]
18721881

1882+
if (pos.y + pg.pageHeight < viewTop - pad) continue
1883+
if (pos.y > viewBottom + pad) continue
1884+
if (pos.x + pg.pageWidth < viewLeft - pad) continue
1885+
if (pos.x > viewRight + pad) continue
1886+
18731887
// Drop shadow
18741888
ctx.fillStyle = shadowColor
18751889
ctx.fillRect(pos.x + shadowOffset, pos.y + shadowOffset, pg.pageWidth, pg.pageHeight)
@@ -2262,8 +2276,36 @@ function scorePageLayout(drawing, data, staves, stavePointers, ctx, canvas) {
22622276

22632277
// --- Footer ---
22642278
var { copyright1, copyright2 } = data.info || {}
2279+
2280+
// In page mode, render copyright on the canvas at the bottom of page 1
2281+
// instead of in the DOM footer (which is used for scroll/wrap modes).
2282+
var copyrightText = [copyright1, copyright2].filter(Boolean).join(' \u2014 ')
2283+
if (copyrightText) {
2284+
var copyrightDraw = new Claire.Text(copyrightText, 0, {
2285+
font: Math.round(titleFs * 0.32) + 'px ' + getMusicTextFamily(),
2286+
textAlign: 'center',
2287+
})
2288+
// Position in the bottom margin of page 1, above the page number
2289+
copyrightDraw.moveTo(titleCenterX, page1Pos.y + PAGE_H - margins.bottom * 0.55)
2290+
drawing.add(copyrightDraw)
2291+
}
2292+
2293+
// --- Page numbers ---
2294+
var pageNumFont = Math.round(titleFs * 0.36) + 'px ' + getMusicTextFamily()
2295+
for (var pi = 0; pi < pageCount; pi++) {
2296+
var pos = pagePositions[pi]
2297+
var pageNumDraw = new Claire.Text(String(pi + 1), 0, {
2298+
font: pageNumFont,
2299+
textAlign: 'center',
2300+
})
2301+
// Position at bottom center of each page, in the margin area
2302+
pageNumDraw.moveTo(pos.x + PAGE_W / 2, pos.y + PAGE_H - margins.bottom * 0.3)
2303+
drawing.add(pageNumDraw)
2304+
}
2305+
2306+
// Clear DOM footer in page mode (content is on the canvas)
22652307
var footerEl = document.getElementById('footer')
2266-
if (footerEl) footerEl.innerText = (copyright1 || '') + '\n' + (copyright2 || '')
2308+
if (footerEl) footerEl.innerText = ''
22672309

22682310
// --- Store page geometry for quickDraw background rendering ---
22692311
_pageGeometry = {

src/main.js

Lines changed: 85 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -950,15 +950,20 @@ if (pageViewModeSelect) {
950950

951951
function updatePageNavVisibility() {
952952
const nav = document.getElementById('page_nav')
953-
if (nav) nav.style.display = (getLayoutMode() === 'page' && getPageViewMode() === 'single-page') ? 'inline' : 'none'
953+
if (nav) nav.style.display = getLayoutMode() === 'page' ? 'inline' : 'none'
954954
}
955955

956956
function updatePageNav() {
957+
const input = document.getElementById('page_input')
957958
const indicator = document.getElementById('page_indicator')
958959
const prevBtn = document.getElementById('page_prev')
959960
const nextBtn = document.getElementById('page_next')
960961
const totalPages = window._pageGeometry?.pageCount || 1
961-
if (indicator) indicator.textContent = `${currentPageIdx + 1} / ${totalPages}`
962+
if (input) {
963+
input.value = currentPageIdx + 1
964+
input.max = totalPages
965+
}
966+
if (indicator) indicator.textContent = ` / ${totalPages}`
962967
if (prevBtn) prevBtn.disabled = currentPageIdx <= 0
963968
if (nextBtn) nextBtn.disabled = currentPageIdx >= totalPages - 1
964969
}
@@ -986,6 +991,84 @@ const pageNextBtn = document.getElementById('page_next')
986991
if (pagePrevBtn) pagePrevBtn.onclick = () => scrollToPage(currentPageIdx - 1)
987992
if (pageNextBtn) pageNextBtn.onclick = () => scrollToPage(currentPageIdx + 1)
988993

994+
// Jump-to-page input
995+
const pageInput = document.getElementById('page_input')
996+
if (pageInput) {
997+
pageInput.addEventListener('change', () => {
998+
const totalPages = window._pageGeometry?.pageCount || 1
999+
const page = Math.max(1, Math.min(totalPages, parseInt(pageInput.value, 10) || 1))
1000+
scrollToPage(page - 1)
1001+
})
1002+
pageInput.addEventListener('keydown', (e) => {
1003+
if (e.key === 'Enter') {
1004+
e.target.blur() // triggers change event
1005+
}
1006+
})
1007+
// Prevent scroll-wheel from changing the number input (confusing UX)
1008+
pageInput.addEventListener('wheel', (e) => e.preventDefault(), { passive: false })
1009+
}
1010+
1011+
// Track current page from scroll position in all page view modes
1012+
;(function initPageScrollTracking() {
1013+
const scoreElm = document.getElementById('score')
1014+
if (!scoreElm) return
1015+
1016+
let trackPending = false
1017+
scoreElm.addEventListener('scroll', () => {
1018+
if (getLayoutMode() !== 'page') return
1019+
if (trackPending) return
1020+
trackPending = true
1021+
requestAnimationFrame(() => {
1022+
trackPending = false
1023+
updateCurrentPageFromScroll()
1024+
})
1025+
})
1026+
})()
1027+
1028+
/**
1029+
* Determine which page is currently most visible and update the nav UI.
1030+
*/
1031+
function updateCurrentPageFromScroll() {
1032+
const pg = window._pageGeometry
1033+
if (!pg || !pg.pagePositions) return
1034+
const scoreElm = document.getElementById('score')
1035+
if (!scoreElm) return
1036+
1037+
const zoom = getZoomLevel()
1038+
const viewMode = getPageViewMode()
1039+
1040+
// Compute viewport center in score-space
1041+
let viewCenterX, viewCenterY
1042+
if (viewMode === 'horizontal') {
1043+
viewCenterX = (scoreElm.scrollLeft + scoreElm.clientWidth / 2) / zoom
1044+
viewCenterY = pg.pagePositions[0]?.y + pg.pageHeight / 2 || 0
1045+
} else {
1046+
viewCenterX = pg.pagePositions[0]?.x + pg.pageWidth / 2 || 0
1047+
viewCenterY = (scoreElm.scrollTop + scoreElm.clientHeight / 2) / zoom
1048+
}
1049+
1050+
// Find the page whose center is closest to viewport center
1051+
let closestPage = 0
1052+
let closestDist = Infinity
1053+
for (let i = 0; i < pg.pageCount; i++) {
1054+
const pos = pg.pagePositions[i]
1055+
const pageCenterX = pos.x + pg.pageWidth / 2
1056+
const pageCenterY = pos.y + pg.pageHeight / 2
1057+
const dx = pageCenterX - viewCenterX
1058+
const dy = pageCenterY - viewCenterY
1059+
const dist = dx * dx + dy * dy
1060+
if (dist < closestDist) {
1061+
closestDist = dist
1062+
closestPage = i
1063+
}
1064+
}
1065+
1066+
if (closestPage !== currentPageIdx) {
1067+
currentPageIdx = closestPage
1068+
updatePageNav()
1069+
}
1070+
}
1071+
9891072
// ---------------------------------------------------------------------------
9901073
// Zoom Fit Mode (width / height) — buttons next to zoom slider
9911074
// ---------------------------------------------------------------------------

0 commit comments

Comments
 (0)