Every speed in config.js is in pixels per second, so the same numbers cross a
small screen in less time than a big one: on 1280x800 the frame covers the
width in two thirds of the time it takes on 1920x1080, and the player reads as
hurried.
One factor now sits behind every animation clock — tickSeconds() for the
frame loops, `speed` for the corners timer, and a --motion-stretch custom
property for the two CSS fades — so the Speed switch on the queue page slows
the drift, the glides, the pong rally, the drag beats, the wash lap, the
screenshot intervals and the fades together. It is read per frame rather than
baked into the constants, so flipping the switch lands on the next frame: no
animation restarts, and one part-way through a beat keeps its place.
The trail's stills now age on animation time rather than wall-clock time, so a
still fades at the rate the frame that dropped it is moving. The audio
crossfade is deliberately left out, being tied to the track that is ending.
The flag is saved with the queue, unlike `paused`: it describes the screen the
player is on, which a restart does not change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011vCaS2whbFjExShoPNnS3A
Waiting for the shuffle bag to land on the animation you are working on makes
testing one of them tedious. The queue page now carries an Animation row next
to Background: Auto keeps the per-track cycle, any other button pins the player
to that animation until the mode changes again.
The animation names live in config.js alongside their labels, so the server can
validate them and the queue page can label its buttons; motion.js keys its
registry by the same names.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tg15tLAuT6NrCE1ZxKVgDB
Two frontends sharing one WebSocket-backed state on the server:
- Player: fullscreen poster background with a small frame (30% of the
screen) that drifts diagonally and reflects off the edges. Two stacked
YouTube players hand over with a 5s equal-power crossfade so there is
never silence between tracks.
- Queue: add by URL, drag to reorder, delete, click to jump. Shows title
and length per item, highlights what is playing, and tracks remaining
vs. total playtime.
No API key is required — titles come from YouTube's oEmbed endpoint and
durations from a hidden muted probe player that cues each new video just
to read its length.
The fade is driven by both requestAnimationFrame and the interval tick:
a hidden or throttled tab stops serving frames entirely, which would
otherwise strand a handover half-faded with two tracks audible.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tg15tLAuT6NrCE1ZxKVgDB