The video frame plays the paddle: it slides along the bottom of the screen and nowhere else, a square ball runs the diagonals off the walls, and every ball that gets past the frame puts one on a seven-segment counter in the top right. The frame is solid on all four sides, not just its face, so a ball that comes at its flank or up under it is turned away too. Which side a hit came in on is read from where the ball was before the step, with a shallowest-penetration fallback for the frame sliding into the ball rather than the other way round — without that the ball tunnels in and rattles inside the frame. The face keeps the original's angle rule, where the hit position sets the departure angle, so rallies develop instead of the ball retracing one line; the other three sides are plain reflections. The paddle steers by predictLanding, which unfolds the wall reflections analytically instead of simulating them. A paddle that outruns the ball never loses, so PONG_MISS_CHANCE of the time it aims a width wide and lets one through — otherwise the counter would sit on 00 all night. The digits are SVG polygons built from a segment map, so the era-correct display costs no font file. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H9v2Xoa6eoeGYpK6gp95k9
102 lines
5.3 KiB
Markdown
102 lines
5.3 KiB
Markdown
# Party Player
|
||
|
||
A YouTube playlist player for parties. Two browser windows run side by side:
|
||
|
||
- **Player** (`http://localhost:3000/`) — fullscreen poster background with a
|
||
small video frame that moves around, switching animation on every track (see
|
||
[Animations](#animations)). Consecutive videos overlap in a 5 second
|
||
audio/video crossfade, so there is never silence between tracks.
|
||
- **Queue** (`http://localhost:3000/queue`) — add videos by URL, drag to
|
||
reorder, delete, see how much playtime is left, and pick the background and
|
||
animation.
|
||
|
||
## Run
|
||
|
||
```sh
|
||
npm install
|
||
npm start
|
||
```
|
||
|
||
Put the player on the projector/TV screen (F11 for fullscreen) and open the
|
||
queue on a laptop or phone. On the same network, other devices reach both pages
|
||
at `http://<this-machine-ip>:3000`.
|
||
|
||
**The player page opens behind a "Start" button.** Browsers block unmuted
|
||
autoplay, so the first click is what unlocks audio. One click per session.
|
||
|
||
## Notes
|
||
|
||
- No YouTube API key is needed. Titles come from YouTube's public oEmbed
|
||
endpoint; durations are measured by a hidden muted player on the player page,
|
||
which is why a freshly added video shows `–:––` for a moment (and keeps
|
||
showing it while the player page is closed). Totals are prefixed with `≥`
|
||
whenever a length is still unknown.
|
||
- The queue is saved to `queue.json` on every change and restored at startup.
|
||
- Settings live in `config.js`: overlap length, frame size, drift speed, corner
|
||
screenshot interval and margin, the drag animation's size range, port, and
|
||
the selectable backgrounds and animations. Anything the browser needs must
|
||
also be mirrored in the `/config.js` route in `server/index.js`.
|
||
|
||
## Animations
|
||
|
||
The video frame's movement changes with every track. Six animations ship
|
||
today:
|
||
|
||
- **bounce** — diagonal drift, reflecting off the screen edges.
|
||
- **corners** — the frame parks in a random corner and every 5 seconds a
|
||
screenshot of it appears in the next of the three other corners, going
|
||
clockwise. The frame never moves, so a screenshot can never cover it.
|
||
- **drag** — a drawn mouse pointer travels to the frame, grabs it near its top
|
||
edge and drags it across the screen, lets go for a beat, then takes hold of
|
||
one corner and pulls the frame to a new size. The aspect ratio stays locked
|
||
and the frame stays on screen, so the resize reads as a window resize rather
|
||
than a stretch.
|
||
- **pong** — the 1972 game with the frame cast as the paddle: it slides along
|
||
the bottom of the screen and nowhere else, a square ball runs the diagonals
|
||
off the walls, and where the ball lands on the frame's face sets the angle it
|
||
leaves at. The frame is solid on all four sides, so a ball that comes at its
|
||
flank or up under it is turned away too. It plays the ball's predicted
|
||
landing spot, but aims a width wide
|
||
once in a while, and every ball that gets past it puts one on the
|
||
seven-segment counter in the top right.
|
||
- **trail** — the frame glides along a curving path, steering from waypoint to
|
||
waypoint, and drops a screenshot of itself every frame-width or so. The stills
|
||
stay where they were taken and fade over a few seconds, leaving a comet tail of
|
||
frozen video behind the live one.
|
||
- **wash** — the frame parks in the middle and the colour changes instead: a
|
||
gradient creeps round the colour wheel behind the poster, taking a minute and
|
||
a half per lap, and the frame's glow travels with it. The one still animation,
|
||
which is what makes it worth having between the moving ones.
|
||
|
||
The queue page's **Animation** buttons pin the player to one of them, which is
|
||
how you look at a single animation without waiting for the cycle to reach it.
|
||
**Auto** (the default) hands selection back to the shuffle bag. Switching takes
|
||
effect immediately; while pinned, track changes leave the animation running.
|
||
|
||
**The corners and trail animations ask to share your screen.** The decks are
|
||
cross-origin YouTube iframes, so nothing on the page can read their pixels —
|
||
capturing the tab and cropping out the frame is the only way to photograph what
|
||
is playing. The prompt appears once, on the first click of **Start**; choose
|
||
*this tab*. Declining is fine: the frame still parks in its corner and still
|
||
glides, just without the screenshots. The prompt is skipped entirely on a plain-HTTP origin (i.e. when
|
||
you open the player from another machine over the network), for the same reason.
|
||
|
||
## Adding animations
|
||
|
||
`#frameMover` carries only the motion transform (see `public/player/motion.js`).
|
||
`#frameSkin` inside it is the cosmetic layer, and the two video decks sit inside
|
||
that. New effects should nest a fresh wrapper rather than sharing a transform
|
||
with the bounce loop.
|
||
|
||
An animation is `start(context) => stop()`, where `context` is
|
||
`{ element, capture }` — `capture` may be null, and is read late, so an
|
||
animation must cope with it being absent. `stop()` has to undo everything:
|
||
timers, listeners, and any DOM it added, because the next animation takes over
|
||
the same element immediately. Register it under a new name in the `ANIMATIONS`
|
||
map in `motion.js` **and** in the one in `config.js` (which supplies the label
|
||
for the queue page's button), and it joins the rotation.
|
||
|
||
Selection uses a shuffle bag (`createPicker`): every animation comes up exactly
|
||
once per pass, so they stay evenly used, and the same one never runs twice in a
|
||
row — including across the seam between two passes.
|