Skip to content

Commit fc6d7bb

Browse files
Current page annotations in full screen view
1 parent e329def commit fc6d7bb

5 files changed

Lines changed: 307 additions & 7 deletions

File tree

site/src/AnnotationManager.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,10 @@ export class AnnotationManager extends TimeManagerListener {
302302
}
303303
}
304304

305+
getAllAnnotations(): Annotation[] {
306+
return Object.values(this.allAnnotations).flat();
307+
}
308+
305309
private saveUserAnnotations() {
306310
localStorage.setItem('wozzeck-user-annotations', JSON.stringify(this.allAnnotations["User"]));
307311
}

site/src/CurrentPageAnnotations.ts

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
import {ScoreTime, TimeManagerListener, UpdateSource} from "./TimeManager";
2+
import {Annotation, AnnotationCode} from "./data/annotations";
3+
import {bar_to_page} from "./data/barToPage";
4+
import {globals} from "./globals";
5+
6+
// Maps each annotation code to the CSS custom-property that defines its colour.
7+
// Reading from the computed style keeps the colour values in one place (styles.css).
8+
const CODE_CSS_VAR: Record<AnnotationCode, string> = {
9+
'dy': '--dynamiques-color',
10+
'du': '--duree-color',
11+
'for': '--formes-color',
12+
'int': '--intonation-color',
13+
'mo': '--motifs-color',
14+
'tim': '--timbre-color',
15+
'graph': '--graph-color',
16+
};
17+
18+
function annotationColor(codes: AnnotationCode[]): string {
19+
for (const code of codes) {
20+
if (code !== 'graph') {
21+
const cssVar = CODE_CSS_VAR[code];
22+
return getComputedStyle(document.documentElement).getPropertyValue(cssVar).trim();
23+
}
24+
}
25+
return '';
26+
}
27+
28+
export class CurrentPageAnnotations extends TimeManagerListener {
29+
private readonly container: HTMLElement;
30+
private readonly getAnnotations: () => Annotation[];
31+
private readonly imageHolder: HTMLElement | null;
32+
33+
constructor(getAnnotations: () => Annotation[]) {
34+
super();
35+
this.getAnnotations = getAnnotations;
36+
37+
this.container = document.createElement('div');
38+
this.container.id = 'current-page-annotations';
39+
40+
this.imageHolder = document.getElementById('image-holder');
41+
this.imageHolder?.appendChild(this.container);
42+
43+
// Set up delegated listeners on imageHolder so bar-overlay events are
44+
// caught regardless of when ScoreManager adds or removes those elements.
45+
if (this.imageHolder) {
46+
this.setupBarHoverDelegation(this.imageHolder);
47+
}
48+
}
49+
50+
async timeUpdated(scoreTime: ScoreTime, _updateSource: UpdateSource) {
51+
this.render(scoreTime);
52+
}
53+
54+
// ── Page membership ───────────────────────────────────────
55+
56+
private barsOnCurrentPage(scoreTime: ScoreTime): Set<number> {
57+
const currentImage = bar_to_page[scoreTime.act - 1][scoreTime.bar].image;
58+
const actBars = bar_to_page[scoreTime.act - 1];
59+
const result = new Set<number>();
60+
for (const barNum in actBars) {
61+
if (actBars[barNum].image === currentImage) {
62+
result.add(Number(barNum));
63+
}
64+
}
65+
return result;
66+
}
67+
68+
private annotationsForPage(scoreTime: ScoreTime): Annotation[] {
69+
const onPage = this.barsOnCurrentPage(scoreTime);
70+
return this.getAnnotations().filter(annotation => {
71+
if (annotation.act !== scoreTime.act) return false;
72+
for (let bar = annotation.measure_range[0]; bar <= annotation.measure_range[1]; bar++) {
73+
if (onPage.has(bar)) return true;
74+
}
75+
return false;
76+
});
77+
}
78+
79+
private highlightBarsForAnnotation(range: [number, number], codes: AnnotationCode[]) {
80+
if (!this.imageHolder) return;
81+
const color = annotationColor(codes);
82+
for (const el of this.imageHolder.querySelectorAll<HTMLElement>('[data-bar]')) {
83+
const bar = Number(el.dataset.bar);
84+
const inRange = bar >= range[0] && bar <= range[1];
85+
el.classList.toggle('annotation-highlight', inRange);
86+
// Override the default pink background with the annotation-category colour.
87+
// Inline styles beat class rules, so this shows through the opacity set by
88+
// .annotation-highlight. Cleared back to '' when the hover ends.
89+
el.style.background = inRange && color ? color : '';
90+
}
91+
}
92+
93+
private clearBarHighlights() {
94+
this.imageHolder
95+
?.querySelectorAll<HTMLElement>('[data-bar].annotation-highlight')
96+
.forEach(el => {
97+
el.classList.remove('annotation-highlight');
98+
el.style.background = '';
99+
});
100+
}
101+
102+
// ── Hover: bar overlays → annotation divs (delegated) ────
103+
104+
private setupBarHoverDelegation(imageHolder: HTMLElement) {
105+
imageHolder.addEventListener('mouseover', (e: MouseEvent) => {
106+
const from = (e.relatedTarget as HTMLElement | null)
107+
?.closest<HTMLElement>('[data-bar]');
108+
const to = (e.target as HTMLElement)
109+
.closest<HTMLElement>('[data-bar]');
110+
// Only act when entering a different bar overlay (ignores moves within one).
111+
if (to && to !== from) {
112+
this.highlightAnnotationsForBar(Number(to.dataset.bar));
113+
}
114+
});
115+
116+
imageHolder.addEventListener('mouseout', (e: MouseEvent) => {
117+
const from = (e.target as HTMLElement)
118+
.closest<HTMLElement>('[data-bar]');
119+
const to = (e.relatedTarget as HTMLElement | null)
120+
?.closest<HTMLElement>('[data-bar]');
121+
// Only clear when leaving a bar overlay entirely (ignores moves within one).
122+
if (from && from !== to) {
123+
this.clearAnnotationHighlights();
124+
}
125+
});
126+
}
127+
128+
private highlightAnnotationsForBar(bar: number) {
129+
for (const el of this.container.querySelectorAll<HTMLElement>('.page-annotation')) {
130+
const start = Number(el.dataset.measureStart);
131+
const end = Number(el.dataset.measureEnd);
132+
el.classList.toggle('annotation-highlight', bar >= start && bar <= end);
133+
}
134+
}
135+
136+
private clearAnnotationHighlights() {
137+
this.container
138+
.querySelectorAll<HTMLElement>('.page-annotation.annotation-highlight')
139+
.forEach(el => el.classList.remove('annotation-highlight'));
140+
}
141+
142+
// ── Render ────────────────────────────────────────────────
143+
144+
private render(scoreTime: ScoreTime) {
145+
const annotations = this.annotationsForPage(scoreTime);
146+
147+
this.container.classList.toggle('has-annotations', annotations.length > 0);
148+
this.container.innerHTML = '';
149+
150+
for (const annotation of annotations) {
151+
const div = document.createElement('div');
152+
div.classList.add('page-annotation');
153+
for (const code of annotation.code) {
154+
div.classList.add(`${code}-annotation`);
155+
}
156+
if (annotation.code.length === 0) {
157+
div.classList.add('unclassified-annotation');
158+
}
159+
160+
// Store measure range as data attributes so the bar-hover handler
161+
// can determine which annotations to highlight without holding a
162+
// closure over the full annotation object for each bar overlay.
163+
div.dataset.measureStart = String(annotation.measure_range[0]);
164+
div.dataset.measureEnd = String(annotation.measure_range[1]);
165+
166+
div.addEventListener('mouseenter', () =>
167+
this.highlightBarsForAnnotation(annotation.measure_range, annotation.code));
168+
div.addEventListener('mouseleave', () =>
169+
this.clearBarHighlights());
170+
171+
const text = document.createElement('div');
172+
text.classList.add('page-annotation-text');
173+
text.innerHTML = annotation.annotation[globals.language];
174+
div.appendChild(text);
175+
176+
this.container.appendChild(div);
177+
}
178+
}
179+
}

site/src/ScoreManager.ts

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import {bar_to_page, BarInfo} from "./data/barToPage";
44
export class ScoreManager extends TimeManagerListener {
55
private currentPage: undefined | string;
66
private currentAct: undefined | number;
7+
private rebuildTimer: ReturnType<typeof setTimeout> | null = null;
8+
79
constructor(tm : TimeManager) {
810
super();
911
this.currentPage = undefined;
@@ -17,6 +19,27 @@ export class ScoreManager extends TimeManagerListener {
1719
<img class="score-page-image" id="score-viewer-image"/>
1820
</div>`
1921
}
22+
23+
// Recalculate overlay positions whenever the score image is resized.
24+
// ResizeObserver fires after layout, so im.width/height and the image's
25+
// position within #image-holder are already up-to-date when the callback
26+
// runs. This covers window resizes, entering fullscreen, and exiting
27+
// fullscreen without any per-trigger wiring in ScoreTransportOverlay.
28+
const img = document.getElementById('score-viewer-image') as HTMLImageElement | null;
29+
if (img) {
30+
new ResizeObserver(() => {
31+
// Skip the initial synchronous callback that fires on observe() —
32+
// no page has loaded yet at construction time.
33+
if (this.currentPage === undefined) return;
34+
35+
// Debounce: window resizes fire many times per second.
36+
if (this.rebuildTimer !== null) clearTimeout(this.rebuildTimer);
37+
this.rebuildTimer = setTimeout(() => {
38+
this.rebuildTimer = null;
39+
this.rebuildOveralysAtCurrentTime();
40+
}, 50);
41+
}).observe(img);
42+
}
2043
}
2144

2245
async preloadTime(time: ScoreTime) {
@@ -41,14 +64,29 @@ export class ScoreManager extends TimeManagerListener {
4164
}
4265
}
4366

44-
private positionOverlay(overlay: HTMLElement, barInfo: BarInfo, w: number, h: number) {
45-
overlay.style.top = (barInfo.y * h) + "px";
46-
overlay.style.left = (barInfo.x * w) + "px";
47-
overlay.style.width = (barInfo.w * w) + "px";
67+
// Returns how far the rendered image is offset from the top-left corner of
68+
// #image-holder. In normal mode this is (0, 0); in fullscreen the image is
69+
// centred, so there may be horizontal and/or vertical whitespace.
70+
private imageOffset(): { x: number; y: number } {
71+
const imageHolder = document.getElementById('image-holder');
72+
const im = document.getElementById('score-viewer-image') as HTMLImageElement | null;
73+
if (!imageHolder || !im) return { x: 0, y: 0 };
74+
const hr = imageHolder.getBoundingClientRect();
75+
const ir = im.getBoundingClientRect();
76+
return { x: ir.left - hr.left, y: ir.top - hr.top };
77+
}
78+
79+
private positionOverlay(overlay: HTMLElement, barInfo: BarInfo,
80+
w: number, h: number,
81+
offsetX: number, offsetY: number) {
82+
overlay.style.top = (barInfo.y * h + offsetY) + "px";
83+
overlay.style.left = (barInfo.x * w + offsetX) + "px";
84+
overlay.style.width = (barInfo.w * w) + "px";
4885
overlay.style.height = (barInfo.h * h) + "px";
4986
}
5087

5188
rebuildOveralysAtCurrentTime() {
89+
if (this.currentPage === undefined) return;
5290
this.rebuildPageOverlays(this.timeManager.scoreTime);
5391
}
5492

@@ -61,6 +99,8 @@ export class ScoreManager extends TimeManagerListener {
6199
const im = document.getElementById('score-viewer-image') as HTMLImageElement;
62100
const w = im.width;
63101
const h = im.height;
102+
const { x: offsetX, y: offsetY } = this.imageOffset();
103+
64104
const currentImage = bar_to_page[scoreTime.act - 1][scoreTime.bar].image;
65105
const actBars = bar_to_page[scoreTime.act - 1];
66106

@@ -79,7 +119,7 @@ export class ScoreManager extends TimeManagerListener {
79119
this.timeManager.goToTime(scoreTime.act, parseInt(barNum), 1, "score-click");
80120
});
81121
}
82-
this.positionOverlay(div, barInfo, w, h);
122+
this.positionOverlay(div, barInfo, w, h, offsetX, offsetY);
83123
imageHolder.appendChild(div);
84124
}
85125
}

site/src/main.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {ArchitectureManager} from "./ArchitectureManager";
99
import {TitleSectionManager} from "./TitleSectionManager";
1010
import {ScoreTransportOverlay} from "./ScoreTransportOverlay";
1111
import {VideoPlayerManager} from "./VideoPlayerManager";
12+
import {CurrentPageAnnotations} from "./CurrentPageAnnotations";
1213

1314
function buildWindow(lang : LanguageCode ) {
1415
globals.language = lang;
@@ -40,6 +41,7 @@ function buildWindow(lang : LanguageCode ) {
4041
let transportManager = new TransportManager(timeManager);
4142
let timelineManager = new TimelineManager(timeManager);
4243
let annotationManager = new AnnotationManager(timeManager);
44+
let currentPageAnnotations = new CurrentPageAnnotations(() => annotationManager.getAllAnnotations());
4345
let architectureManager = new ArchitectureManager(timeManager);
4446
let videoPlayerManager = new VideoPlayerManager(timeManager);
4547
new TitleSectionManager();
@@ -49,6 +51,7 @@ function buildWindow(lang : LanguageCode ) {
4951
timeManager.listeners.push(transportManager);
5052
timeManager.listeners.push(timelineManager);
5153
timeManager.listeners.push(annotationManager);
54+
timeManager.listeners.push(currentPageAnnotations);
5255
timeManager.listeners.push(architectureManager);
5356
timeManager.listeners.push(videoPlayerManager);
5457

0 commit comments

Comments
 (0)