Files
party-player/public/player/player.css
T
simonandClaude Opus 5 ce1a6d6a47 Cycle the frame through animations, add a corners effect
The frame had exactly one behaviour, started once and run forever. Make
animations a start(context) => stop() lifecycle held in a registry, and
draw a new one on every completed crossfade.

Selection uses a shuffle bag: each animation comes up exactly once per
pass, so they stay evenly used, and a swap at the pass boundary keeps the
same one from running twice in a row. Written for a registry larger than
today's two entries.

The new corners animation parks the frame in a random corner and drops a
screenshot of it into the next of the other three corners every 5s, going
clockwise. The frame's own corner is never a slot, so a still cannot
cover it by construction rather than by clamping. This holds while
FRAME_SCALE stays under 0.5, now noted where that knob lives.

Screenshots need getDisplayMedia: the decks are cross-origin YouTube
iframes, so canvas taints and there is no other way to read the playing
pixels. The prompt has to be requested before start() yields, or the
click's transient activation is gone. Declining, or serving over plain
HTTP where mediaDevices is undefined, leaves the frame parked without
stills instead of failing.

The capture sink is a 1x1 element in the document, like #probe: detached
video elements are never composited and get throttled hard enough to make
every screenshot seconds stale.

startBounce also gains the stop() it always needed — it was leaking its
rAF loop and resize listener, and a queued frame would fight whichever
animation came next.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tg15tLAuT6NrCE1ZxKVgDB
2026-08-09 22:04:33 +02:00

201 lines
3.7 KiB
CSS

:root {
--accent: #ff4fc3;
--ink: #14060f;
}
* {
box-sizing: border-box;
}
html,
body {
margin: 0;
height: 100%;
overflow: hidden;
background: var(--ink);
font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
cursor: none;
}
/* --- background ------------------------------------------------------- */
#backgrounds {
position: fixed;
inset: 0;
}
#backgrounds img {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
opacity: 0;
transition: opacity 900ms ease;
/* Knock the poster back so the video frame stays the focal point — the
posters are pale and read far brighter on a projector than on a desk. */
filter: brightness(0.45) saturate(1.15);
}
#backgrounds img.active {
opacity: 1;
}
/* --- frozen screenshots ------------------------------------------------ */
#stills {
position: fixed;
inset: 0;
pointer-events: none;
z-index: 1;
}
#stills img {
position: absolute;
top: 0;
left: 0;
object-fit: cover;
opacity: 0;
/* Matches #frameSkin, so a still reads as a frozen copy of the frame. */
border-radius: 10px;
border: 2px solid rgba(255, 255, 255, 0.85);
box-shadow:
0 0 0 4px rgba(255, 79, 195, 0.55),
0 24px 70px rgba(0, 0, 0, 0.7);
transition: opacity 500ms ease;
will-change: transform;
}
#stills img.visible {
opacity: 1;
}
/* --- the moving frame -------------------------------------------------- */
#frameMover {
position: fixed;
top: 0;
left: 0;
/* Above the stills, so the live video always wins if anything ever overlaps. */
z-index: 2;
will-change: transform;
}
#frameSkin {
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
border-radius: 10px;
border: 2px solid rgba(255, 255, 255, 0.85);
box-shadow:
0 0 0 4px rgba(255, 79, 195, 0.55),
0 24px 70px rgba(0, 0, 0, 0.7);
background: #000;
}
.deck {
position: absolute;
inset: 0;
opacity: 0;
/* Opacity is driven frame-by-frame during a crossfade, so no CSS transition
here — it would fight the JS ramp. */
}
.deck iframe {
width: 100%;
height: 100%;
border: 0;
/* The iframe would otherwise swallow nothing useful, but keeping it inert
avoids stray clicks pausing playback. */
pointer-events: none;
}
/* The screen-capture stream's sink. Has to be in the document to keep getting
frames (see capture.js), but must not be visible or catch clicks. */
#captureSink {
position: fixed;
left: 0;
top: 0;
width: 1px;
height: 1px;
opacity: 0.01;
pointer-events: none;
z-index: -1;
}
#probe {
position: fixed;
left: -400px;
top: -400px;
width: 320px;
height: 180px;
opacity: 0;
pointer-events: none;
}
/* --- start gate -------------------------------------------------------- */
#gate {
position: fixed;
inset: 0;
display: grid;
place-items: center;
background: rgba(20, 6, 15, 0.92);
backdrop-filter: blur(6px);
z-index: 10;
cursor: pointer;
transition: opacity 400ms ease;
}
#gate.hidden {
opacity: 0;
pointer-events: none;
}
.gate-inner {
text-align: center;
color: #fff;
}
.gate-inner h1 {
font-size: clamp(2rem, 7vw, 5rem);
margin: 0 0 0.3em;
letter-spacing: -0.03em;
color: var(--accent);
}
.gate-inner p {
margin: 0 0 2em;
opacity: 0.7;
font-size: 1.05rem;
}
#start {
font: inherit;
font-size: 1.3rem;
padding: 0.7em 3em;
border: 0;
border-radius: 999px;
background: var(--accent);
color: var(--ink);
font-weight: 700;
cursor: pointer;
}
#start:disabled {
opacity: 0.35;
cursor: default;
}
#status {
position: fixed;
bottom: 12px;
right: 16px;
z-index: 20;
color: var(--accent);
font-size: 0.8rem;
letter-spacing: 0.08em;
text-transform: uppercase;
}