Skip to content
This repository was archived by the owner on May 14, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 24 additions & 39 deletions frontend/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -91,60 +91,45 @@
}
}

/* Streaming card enter: materialize from noise/static */
/*
Streaming card enter: pixels coalesce into the card.
Steps through px-dissolve-5 (fully masked) → px-dissolve-0 (fully opaque).
Filter URLs don't interpolate, so each keyframe snaps to a new mask state;
with 6 steps over ~1.4s the cadence reads as a stuttering pixel reveal.
*/
@keyframes card-enter {
0% {
opacity: 0;
transform: scale(0.9);
filter: url(#pixel-dissolve) blur(8px) contrast(2) brightness(1.4) saturate(1.6);
}
40% {
opacity: 0.6;
filter: url(#pixel-dissolve) blur(3px) contrast(1.4) brightness(1.15);
}
75% {
opacity: 0.95;
filter: blur(1px) contrast(1.05);
}
100% {
opacity: 1;
transform: scale(1);
filter: none;
}
0% { transform: scale(0.94); filter: url(#px-dissolve-5); }
20% { transform: scale(0.96); filter: url(#px-dissolve-4); }
40% { transform: scale(0.97); filter: url(#px-dissolve-3); }
60% { transform: scale(0.98); filter: url(#px-dissolve-2); }
80% { transform: scale(0.99); filter: url(#px-dissolve-1); }
100% { transform: scale(1); filter: none; }
}

/* Streaming card exit: dissolve into pixels/noise and fade */
/* Streaming card exit: pixels fall away, mirror of enter. */
@keyframes card-exit {
0% {
opacity: 1;
transform: scale(1);
filter: none;
}
40% {
opacity: 0.7;
filter: url(#pixel-dissolve) blur(2px) contrast(1.5) brightness(1.1);
}
100% {
opacity: 0;
transform: scale(0.94);
filter: url(#pixel-dissolve) blur(10px) contrast(2.5) brightness(0.7) saturate(1.8);
}
0% { transform: scale(1); filter: url(#px-dissolve-0); }
20% { transform: scale(0.99); filter: url(#px-dissolve-1); }
40% { transform: scale(0.98); filter: url(#px-dissolve-2); }
60% { transform: scale(0.97); filter: url(#px-dissolve-3); }
80% { transform: scale(0.96); filter: url(#px-dissolve-4); }
100% { transform: scale(0.94); filter: url(#px-dissolve-5); }
}

.streaming-card {
animation: card-enter 600ms cubic-bezier(0.22, 0.61, 0.36, 1) both;
will-change: opacity, transform, filter;
animation: card-enter 1400ms cubic-bezier(0.22, 0.61, 0.36, 1) both;
will-change: transform, filter;
}

.streaming-card.streaming-active {
animation:
card-enter 600ms cubic-bezier(0.22, 0.61, 0.36, 1) both,
pulse-glow 3s ease-in-out 600ms infinite;
card-enter 1400ms steps(6, end) both,
pulse-glow 3s ease-in-out 1400ms infinite;
}

.streaming-card-exit,
.streaming-card-exit.streaming-active {
animation: card-exit 550ms cubic-bezier(0.4, 0, 1, 1) both;
animation: card-exit 1200ms cubic-bezier(0.4, 0, 1, 1) both;
pointer-events: none;
}

Expand Down
35 changes: 29 additions & 6 deletions frontend/src/pages/Dashboard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -167,9 +167,11 @@ function Dashboard() {
});

const vanishedIds = new Set(vanished.map(s => s.id));
// Must match the .streaming-card-exit animation duration in index.css.
// Removing the element earlier truncates the dissolve mid-animation.
const timer = setTimeout(() => {
setExitingSessions(prev => prev.filter(s => !vanishedIds.has(s.id)));
}, 420);
}, 1200);
return () => clearTimeout(timer);
}, [activity]);

Expand Down Expand Up @@ -612,13 +614,34 @@ function Dashboard() {
</div>
)}

{/* SVG filter defs used by streaming-card enter/exit animations */}
{/*
Pixel-dissolve filter variants used by streaming-card enter/exit animations.
Each filter shares the same feTurbulence seed/baseFrequency so the noise
pattern is stable; only the alpha-threshold (intercept on feFuncA) shifts,
which masks out a progressively larger fraction of the source pixels.
The CSS @keyframes step through #px-dissolve-0 (fully opaque) →
#px-dissolve-5 (fully masked) for a true "pixels falling away" look
instead of the smooth warping that feDisplacementMap produced.
*/}
<svg aria-hidden="true" width="0" height="0" className="absolute pointer-events-none" style={{ position: 'absolute' }}>
<defs>
<filter id="pixel-dissolve" x="-20%" y="-20%" width="140%" height="140%">
<feTurbulence type="fractalNoise" baseFrequency="0.06" numOctaves="2" seed="7" stitchTiles="stitch" result="noise" />
<feDisplacementMap in="SourceGraphic" in2="noise" scale="28" xChannelSelector="R" yChannelSelector="G" />
</filter>
{[
{ id: 'px-dissolve-0', intercept: 20 }, // all pixels survive
{ id: 'px-dissolve-1', intercept: -2 }, // ~95% survive
{ id: 'px-dissolve-2', intercept: -6 }, // ~75% survive
{ id: 'px-dissolve-3', intercept: -10 }, // ~50% survive
{ id: 'px-dissolve-4', intercept: -14 }, // ~25% survive
{ id: 'px-dissolve-5', intercept: -20 }, // none survive
].map(({ id, intercept }) => (
<filter key={id} id={id} x="-10%" y="-10%" width="120%" height="120%">
<feTurbulence type="fractalNoise" baseFrequency="0.18" numOctaves="2" seed="7" stitchTiles="stitch" result="noise" />
<feColorMatrix in="noise" type="luminanceToAlpha" result="lumNoise" />
<feComponentTransfer in="lumNoise" result="mask">
<feFuncA type="linear" slope="20" intercept={intercept} />
</feComponentTransfer>
<feComposite in="SourceGraphic" in2="mask" operator="in" />
</filter>
))}
</defs>
</svg>

Expand Down
Loading