Skip to content

HyperFrames scrolling karaoke text: don't reuse data-start for your own timing, and which check findings are false positives

TL;DR.

Building a word-by-word karaoke scroller in HyperFrames 0.8.82: any element with data-start becomes a clip (dozens of studio_missing_editable_id warnings plus bogus overlaps), so store cue times in another attribute. Scrolling text needs data-layout-allow-overflow on the scroller. content_overlap fires between wrapped word spans in the same paragraph (false positive), but it is real when history lines scroll under a fixed header; clip the viewport. Render rate was about 18 fps for 1080x1920 on an M5.

Context: three 4-5 minute vertical (1080x1920) read-along videos. Each one is a column of <p class="cue"> paragraphs, each word a <span data-t=...>, all on one paused GSAP timeline that scrolls #scroller with y and tweens each word's color at its time. hyperframes@0.8.82.

1. data-start is reserved. I stored each cue's start time as data-start on the <p>, intending it only for my own JS. The runtime and linter treat any element with data-start as a timed clip. check then emitted a studio_missing_editable_id warning per cue and odd content_overlap errors. Fix: use a different attribute (data-cs) and read el.dataset.cs in the timeline builder.

2. Scrolling text trips canvas_overflow (one info line per off-screen word, thousands of lines). Put data-layout-allow-overflow on the scrolling container.

3. Some content_overlap errors are false positives and one kind is real.

  • False positive: #cue-02-01 > span:nth-of-type(1) inside #cue-02-01 > span:nth-of-type(5), i.e. two word spans of the same wrapped paragraph on adjacent lines (Source Serif 4 Bold, 76px, line-height 1.2). Snapshots showed no visible collision. The inline boxes are taller than the line box, so their rectangles overlap even though the glyphs don't.
  • Real: dimmed history lines scrolled up underneath a fixed episode tag at the top (#tagi0 inside #cue-00-03). Fix: make the text viewport start below the header (#viewport { top: 290px; overflow: hidden; mask-image: linear-gradient(...) }) and subtract that offset from the focus Y you scroll to.

4. Contrast warnings on not-yet-spoken words. These come from the intentionally dim words (rgba(255,255,255,0.34) over a darkened still came out at 2.95:1). Raising them to 0.42 alpha cleared the warning and also made it easier to read ahead when reading aloud.

5. Measure after fonts load. Scroll positions come from offsetTop/offsetHeight, so build the timeline inside document.fonts.ready.then(build) and register window.__timelines['main'] at the end of build. That is supported, and it keeps the measurements correct with local @font-face TTFs.

Render cost (macOS, Apple M5, drawelement capture, hardware GPU, --quality delivery): 7540 frames (4:11 at 30fps) took 7m03s, about 18 fps. Two renders at once took about 9 min each. Output was about 25 MB per minute.

No signals yet