feat(overlay): ticker scroll mode with a speed control - #36
Conversation
scroll picks the message layout over time (off, ticker) and scrollspeed is the ticker lane's multiplier, 1 to 5 in whole steps. Both go through the usual four-file contract so the Zod schema and the dependency-free OBS parser cannot drift, with parity cases for the enum, the clamp and every malformed shape. lib/overlay/ticker.ts holds the scheduling math on its own: duration from the row's own width so every message moves at the same speed, and a shared cursor so the next one does not start on the back of the one ahead. planTickerRun returns null past TICKER_HORIZON_MS rather than queueing, because a lane carries far less than a busy channel sends and an unbounded cursor would run minutes behind real time. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
hb-messages becomes a one-line lane and each row is absolutely positioned past the clip edge, carried across by the hb-ticker keyframe. MessageList measures the row in a stable callback ref and writes the animation shorthand on the node, so ChatMessageRow stays memoized and never puts animation in its style prop for React to clear. align picks the direction rather than an edge to hug: right runs right to left, left is the same run reversed. layout=stacked folds back to inline in HbRoot instead of a CSS override, which would have outranked the group rule. Emote, cheermote and badge art reserves a square min-width, or a row measures short before its art loads and collides with its neighbour. The clock is document.timeline.currentTime, not performance.now(): a hidden document freezes it, so the cursor freezes with the animations rather than dumping a backlog when OBS shows the source again. A reduced-motion preference turns the mode off outright. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Motion and Ticker speed sit with the layout controls, and the speed buttons only appear once the ticker is on. Every slider in the form is now a row of named stops, so the text size, emote size and refresh controls read like the rest of the builder. The stops had to grow to cover the ranges the sliders used to reach: text size gains 2XL and 3XL to touch the schema's 300 ceiling, emote size gains the two missing half steps, and refresh becomes Off through 24 h. Dropping htmlFor also turns each one into a real fieldset and legend instead of a label pointing at a single input. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The reference gets scroll and scrollspeed with their own anchors, and the align entry now says it picks the lane's direction. The honest part is the ceiling: a lane fits roughly one message every eight seconds at 1x, so on a fast channel the ticker shows a sample of recent chat rather than all of it, and saying so up front is better than a streamer working it out live. CLAUDE.md records the two invariants that look like details and are not: the document timeline is the clock, and a message past the horizon is expired rather than parked. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (23)
WalkthroughThe overlay now supports ChangesTicker scrolling
Estimated code review effort: 4 (Complex) | ~45 minutes Poem
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
What
Adds a horizontal ticker as an option for the overlay: chat lays out as a single lane sliding across the source instead of a column stacking up the side. Two new URL params:
scrolloff,tickeroffscrollspeed1to5, whole steps1It is an orthogonal param rather than a theme, so it composes with all 31 themes.
?theme=neon&scroll=ticker&scrollspeed=3is a neon ticker.alignstops being a dead control in this mode and picks the direction:rightruns right to left the way a news ticker does,leftruns the way you read. It is the same keyframe played in reverse.1x is deliberately a crawl (50 px/s, about 45 seconds to cross a 1920-wide source) and every step climbs from there.
The honest limitation
A lane has a throughput ceiling that a column does not. One message occupies it for the whole time it takes to cross, so it fits roughly one message every eight seconds at 1x. A busy channel sends 10 to 40 a second.
Anything that cannot start within six seconds is dropped rather than queued, so the lane shows a sample of recent chat instead of a backlog running minutes behind. This is stated in
/docsunderscroll, because it is the first thing a streamer would otherwise discover live.Dropping is not optional. Parking those rows instead looks harmless and is not: a run lasts 10-20s while a busy channel refills
?maxin about three, so unscheduled rows push the in-flight ones out of the list and the lane goes permanently blank. Only messages actually flying hold a slot. Caught in the browser against xqc, not in review.Notes on the implementation
setTimeoutanywhere. The whole schedule ridesanimation-delay, since OBS throttles JS timers while a source is hidden and CSS animation clocks keep running.document.timeline.currentTime, notperformance.now(). A hidden document freezes the timeline, so the cursor freezes with the animations instead of racing ahead. Its reading is legitimately0while hidden, so the fallback cannot be a truthiness check.--hb-ticker-wis written per row, not on the lane. A shared variable handed a later row's re-measurement to every message already in flight while their durations stayed, which reads as a speed jump mid-run.ChatMessageRowstays memoized: the scheduling lives in a[]-dep callback inMessageListand the row hands back its own id.min-widthon each. Without it a four-badge row under-measures by ~74px and collides with its neighbour.lib/overlay/ticker.ts, with its own tests.Also in here
Every range slider in the configurator is now a row of named stops, which is the UI the rest of the builder already used. The stops grew to cover what the sliders reached: text size gains 2XL and 3XL to touch the schema's 300 ceiling, emote size gains the two missing half steps, refresh becomes Off through 24 h. Dropping
htmlForalso turns each into a realfieldset/legendrather than a label pointing at one input.Verified
bun test(241 pass),bun run check-types,bun run checkall green.In the browser against xqc:
lane - (lane + width) / 2).travel / durationis exactly 250 px/s at 5x across every row, so speed is genuinely constant.align=rightreportsnormaland travels 1271 -> 478 -> -316; defaultalign=leftreportsreverseand travels -199 -> 536 -> 1271.bg=bubblerows are content-width, not lane-width.bg=panelis a strip.layout=stackedrenders identically to inline.theme=noboxno longer fades the glyph tops.emotescale=4grows the lane from 37px to 114px without clipping.input[type=range]left, and the buttons produce?scroll=ticker&scrollspeed=4,size=300,refresh=360.Not verified on real hardware: the OBS hide/show behaviour, and the reduced-motion fallback (it reuses the
useReducedMotionthat already gatesstaticMedia).🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Documentation
Bug Fixes