k-l-lambda commited on
Commit
13e8a7a
·
1 Parent(s): 723e020

enhanced score player.

Browse files
Files changed (2) hide show
  1. web/score-player.css +16 -1
  2. web/score-player.js +73 -7
web/score-player.css CHANGED
@@ -27,14 +27,29 @@
27
  .ls-status.ls-err { color: #c0392b; }
28
 
29
  .ls-svg {
30
- background: #fffdf2; /* faint warm/yellow tint behind the score */
31
  display: inline-block;
32
  min-width: 100%;
 
 
 
 
 
33
  }
34
  .ls-svg svg {
35
  max-width: 100%;
36
  height: auto;
37
  }
 
 
 
 
 
 
 
 
 
 
38
  /* Verovio draws everything with stroke|fill="currentColor", which resolves to the
39
  computed `color`. We can't rely on inheritance from a container `color`: Gradio's
40
  `.prose * { color: var(--body-text-color) }` sets color DIRECTLY on every
 
27
  .ls-status.ls-err { color: #c0392b; }
28
 
29
  .ls-svg {
30
+ background: #fff;
31
  display: inline-block;
32
  min-width: 100%;
33
+ position: relative; /* anchor for the absolutely-positioned playback cursor */
34
+ }
35
+ /* faint warm/yellow tint behind the score while generating */
36
+ .ls-svg.ls-generating-bg {
37
+ background: #fffdf2;
38
  }
39
  .ls-svg svg {
40
  max-width: 100%;
41
  height: auto;
42
  }
43
+ /* Playback cursor: a vertical line tracking the currently-sounding note. */
44
+ .ls-cursor {
45
+ position: absolute;
46
+ width: 2px;
47
+ background: rgba(124, 92, 255, 0.85);
48
+ pointer-events: none;
49
+ z-index: 5;
50
+ display: none;
51
+ transition: left 0.06s linear;
52
+ }
53
  /* Verovio draws everything with stroke|fill="currentColor", which resolves to the
54
  computed `color`. We can't rely on inheritance from a container `color`: Gradio's
55
  `.prose * { color: var(--body-text-color) }` sets color DIRECTLY on every
web/score-player.js CHANGED
@@ -46,6 +46,7 @@
46
 
47
  const HIGHLIGHT_THROTTLE_MS = 50;
48
  let lastHighlightUpdate = 0;
 
49
 
50
  function log (msg) { console.log('[LilyScore]', msg); }
51
 
@@ -99,6 +100,8 @@
99
  if (!svg) return;
100
  els.svg.innerHTML = '';
101
  els.svg.appendChild(document.importNode(svg, true));
 
 
102
  }
103
 
104
  function setStatus (text, kind) {
@@ -275,12 +278,57 @@
275
  if (!state.highlighted.has(id)) { const el = document.getElementById(id); if (el) el.classList.add('ls-hl'); }
276
  });
277
  state.highlighted = now;
 
 
 
278
  } catch (e) { /* ignore */ }
279
  }
280
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
281
  function clearHighlights () {
282
  state.highlighted.forEach(function (id) { const el = document.getElementById(id); if (el) el.classList.remove('ls-hl'); });
283
  state.highlighted = new Set();
 
284
  }
285
 
286
  // ---- transport UI -------------------------------------------------------
@@ -323,6 +371,8 @@
323
  const wrap = document.createElement('div'); wrap.className = 'ls-preview';
324
  const status = document.createElement('div'); status.className = 'ls-status';
325
  const svgBox = document.createElement('div'); svgBox.className = 'ls-svg';
 
 
326
  wrap.appendChild(status); wrap.appendChild(svgBox);
327
 
328
  // transport bar
@@ -336,7 +386,7 @@
336
 
337
  root.appendChild(wrap); root.appendChild(player);
338
 
339
- els.root = root; els.svg = svgBox; els.preview = wrap; els.status = status; els.player = player;
340
  els.playBtn = player.querySelector('.ls-play');
341
  els.pauseBtn = player.querySelector('.ls-pause');
342
  els.stopBtn = player.querySelector('.ls-stop');
@@ -352,13 +402,27 @@
352
  const r = els.progress.getBoundingClientRect();
353
  seekTo(state.duration * ((e.clientX - r.left) / r.width));
354
  });
355
- // click a note in the score -> seek there
 
 
 
356
  els.svg.addEventListener('click', function (e) {
357
- if (!state.toolkit || state.generating) return;
358
- const t = e.target.closest && e.target.closest('.note, .chord, .rest');
359
- if (!t || !t.id) return;
360
- const time = state.toolkit.getTimeForElement(t.id);
361
- if (typeof time === 'number') seekTo(time);
 
 
 
 
 
 
 
 
 
 
 
362
  });
363
 
364
  initVerovio(); // warm up early
@@ -375,6 +439,8 @@
375
  flag = !!flag;
376
  if (flag === state.generating) return;
377
  state.generating = flag;
 
 
378
  if (flag) { stop(); }
379
  updatePlayerVisibility();
380
  },
 
46
 
47
  const HIGHLIGHT_THROTTLE_MS = 50;
48
  let lastHighlightUpdate = 0;
49
+ let lastAutoScroll = 0;
50
 
51
  function log (msg) { console.log('[LilyScore]', msg); }
52
 
 
100
  if (!svg) return;
101
  els.svg.innerHTML = '';
102
  els.svg.appendChild(document.importNode(svg, true));
103
+ // re-attach the playback cursor (innerHTML reset above removed it)
104
+ if (els.cursor) { els.cursor.style.display = 'none'; els.svg.appendChild(els.cursor); }
105
  }
106
 
107
  function setStatus (text, kind) {
 
278
  if (!state.highlighted.has(id)) { const el = document.getElementById(id); if (el) el.classList.add('ls-hl'); }
279
  });
280
  state.highlighted = now;
281
+ // move the playback cursor to the first currently-sounding note
282
+ const ids = res.notes || [];
283
+ if (ids.length) updateCursor(ids[0]);
284
  } catch (e) { /* ignore */ }
285
  }
286
 
287
+ // Position the vertical cursor at a note element, spanning its system's height.
288
+ function updateCursor (noteId) {
289
+ if (!els.cursor || !els.svg) return;
290
+ const note = document.getElementById(noteId);
291
+ if (!note) { els.cursor.style.display = 'none'; return; }
292
+ const sysOrStaff = note.closest('.system') || note.closest('.staff');
293
+ const boxRect = els.svg.getBoundingClientRect();
294
+ const noteRect = note.getBoundingClientRect();
295
+ const x = noteRect.left + noteRect.width / 2 - boxRect.left;
296
+ let top = 0, height = boxRect.height;
297
+ if (sysOrStaff) {
298
+ const sr = sysOrStaff.getBoundingClientRect();
299
+ top = sr.top - boxRect.top;
300
+ height = sr.height;
301
+ }
302
+ els.cursor.style.left = x + 'px';
303
+ els.cursor.style.top = top + 'px';
304
+ els.cursor.style.height = height + 'px';
305
+ els.cursor.style.display = 'block';
306
+ // keep the cursor in view while playing
307
+ scrollCursorIntoView(noteRect, boxRect);
308
+ }
309
+
310
+ // Auto-scroll the preview so the cursor stays visible (throttled, gentle).
311
+ function scrollCursorIntoView (noteRect, boxRect) {
312
+ if (!els.preview) return;
313
+ const now = performance.now();
314
+ if (now - lastAutoScroll < 300) return;
315
+ const pr = els.preview.getBoundingClientRect();
316
+ const above = noteRect.top < pr.top + 60;
317
+ const below = noteRect.bottom > pr.bottom - 60;
318
+ if (!above && !below) return;
319
+ lastAutoScroll = now;
320
+ const target = els.preview.scrollTop + (noteRect.top - pr.top) - els.preview.clientHeight * 0.35;
321
+ els.preview.scrollTo({ top: Math.max(0, target), behavior: 'smooth' });
322
+ }
323
+
324
+ function hideCursor () {
325
+ if (els.cursor) els.cursor.style.display = 'none';
326
+ }
327
+
328
  function clearHighlights () {
329
  state.highlighted.forEach(function (id) { const el = document.getElementById(id); if (el) el.classList.remove('ls-hl'); });
330
  state.highlighted = new Set();
331
+ hideCursor();
332
  }
333
 
334
  // ---- transport UI -------------------------------------------------------
 
371
  const wrap = document.createElement('div'); wrap.className = 'ls-preview';
372
  const status = document.createElement('div'); status.className = 'ls-status';
373
  const svgBox = document.createElement('div'); svgBox.className = 'ls-svg';
374
+ const cursor = document.createElement('div'); cursor.className = 'ls-cursor';
375
+ svgBox.appendChild(cursor);
376
  wrap.appendChild(status); wrap.appendChild(svgBox);
377
 
378
  // transport bar
 
386
 
387
  root.appendChild(wrap); root.appendChild(player);
388
 
389
+ els.root = root; els.svg = svgBox; els.preview = wrap; els.status = status; els.player = player; els.cursor = cursor;
390
  els.playBtn = player.querySelector('.ls-play');
391
  els.pauseBtn = player.querySelector('.ls-pause');
392
  els.stopBtn = player.querySelector('.ls-stop');
 
402
  const r = els.progress.getBoundingClientRect();
403
  seekTo(state.duration * ((e.clientX - r.left) / r.width));
404
  });
405
+ // click in the score -> seek to the nearest note/rest. We don't rely on the
406
+ // click target (elementFromPoint often lands on the bare <svg> between
407
+ // noteheads, or on a staff line): instead find the .note/.chord/.rest whose
408
+ // on-screen box is closest to the click, then seek to its time.
409
  els.svg.addEventListener('click', function (e) {
410
+ if (!state.toolkit || state.generating || !state.midiData) return;
411
+ const svg = els.svg.querySelector('svg');
412
+ if (!svg) return;
413
+ const cands = svg.querySelectorAll('.note, .chord, .rest, .mRest');
414
+ let best = null, bestD = Infinity;
415
+ for (var i = 0; i < cands.length; i++) {
416
+ if (!cands[i].id) continue;
417
+ const r = cands[i].getBoundingClientRect();
418
+ const dx = e.clientX - (r.left + r.width / 2);
419
+ const dy = e.clientY - (r.top + r.height / 2);
420
+ const d = dx * dx + dy * dy;
421
+ if (d < bestD) { bestD = d; best = cands[i]; }
422
+ }
423
+ if (!best) return;
424
+ const time = state.toolkit.getTimeForElement(best.id);
425
+ if (typeof time === 'number' && !isNaN(time) && time >= 0) seekTo(time);
426
  });
427
 
428
  initVerovio(); // warm up early
 
439
  flag = !!flag;
440
  if (flag === state.generating) return;
441
  state.generating = flag;
442
+ // faint yellow score background only while generating
443
+ if (els.svg) els.svg.classList.toggle('ls-generating-bg', flag);
444
  if (flag) { stop(); }
445
  updatePlayerVisibility();
446
  },