Files
party-player/public/player/player.css
T
simonandClaude Opus 5 6d306d1b98 Add a comet trail animation
The frame glides 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, so the live video reads as the head of a
comet whose tail is its own recent past.

The steering caps how fast the heading may change: an instant turn would
lay the stills out in straight dotted lines, and it is the curve that
makes a tail read as one. Drops are spaced by distance travelled rather
than by a timer, so they stay even however the frame is turning.

Since the capture is a crop of the tab, a still lying under the frame
would be photographed into the next one. A drop waits until the frame is
clear of the tail, keeping the invariant corners already relies on.

Also picks up the FRAME_SCALE, BOUNCE_SPEED and CORNER_INTERVAL_SECONDS
tuning sitting in the tree.

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

238 lines
4.6 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 trail ramps opacity frame-by-frame as a still ages, so the CSS transition
above would only fight it. */
#stills img.trail {
transition: none;
}
/* --- 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;
}
/* Lift the frame while the drag animation is holding it, the way a dragged
window separates from the desktop. */
#frameMover.dragging #frameSkin {
box-shadow:
0 0 0 4px rgba(255, 79, 195, 0.75),
0 40px 100px rgba(0, 0, 0, 0.85);
}
/* --- the drawn pointer -------------------------------------------------- */
/* Built and removed by the drag animation; nothing else on the page has one. */
#pointer {
position: fixed;
top: 0;
left: 0;
z-index: 3;
pointer-events: none;
will-change: transform;
filter: drop-shadow(0 2px 6px rgba(0, 0, 0, 0.6));
}
#pointer svg {
display: block;
transition: transform 90ms ease;
}
/* The click itself: the arrow dips, so a grab is visible even mid-pause. */
#pointer.down svg {
transform: scale(0.82);
}
.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;
}