diff --git a/packages/studio/src/player/hooks/usePlaybackKeyboard.test.ts b/packages/studio/src/player/hooks/usePlaybackKeyboard.test.ts
index 679cf617e..ab6f7b623 100644
--- a/packages/studio/src/player/hooks/usePlaybackKeyboard.test.ts
+++ b/packages/studio/src/player/hooks/usePlaybackKeyboard.test.ts
@@ -93,7 +93,7 @@ describe("usePlaybackKeyboard — keyboard layout independence (#834)", () => {
dispatch(keydown({ code: "KeyA", key: "a" }));
});
- expect(spies.seek).toHaveBeenCalledWith(1.5);
+ expect(spies.seek).toHaveBeenCalledWith(1.5, { keepPlaying: true });
});
it("'Jump to in-point' fires on AZERTY (physical KeyQ produces e.key='a')", () => {
@@ -104,7 +104,7 @@ describe("usePlaybackKeyboard — keyboard layout independence (#834)", () => {
dispatch(keydown({ code: "KeyQ", key: "a" }));
});
- expect(spies.seek).toHaveBeenCalledWith(2.5);
+ expect(spies.seek).toHaveBeenCalledWith(2.5, { keepPlaying: true });
});
it("AZERTY 'A' physical key (e.key='q') no longer triggers in-point seek", () => {
diff --git a/registry/components/vignette/demo.html b/registry/components/vignette/demo.html
index 522284757..d96e7ef03 100644
--- a/registry/components/vignette/demo.html
+++ b/registry/components/vignette/demo.html
@@ -17,6 +17,7 @@
width: 1920px;
height: 1080px;
overflow: hidden;
+ background: #000;
}
@@ -26,26 +27,22 @@
data-composition-id="vignette-demo"
data-width="1920"
data-height="1080"
- data-duration="6"
+ data-duration="5"
>
+
-
- Vignette
-
-
- Pulls focus toward the center
+
+
+
+
+
+
+
VIGNETTE
+
Frame the eye. Hold the moment.
@@ -67,13 +64,101 @@
.demo-bg {
width: 1920px;
height: 1080px;
- background: radial-gradient(circle at 30% 30%, #c98a4b, #6d3a18 60%, #2a160a 100%);
+ background:
+ radial-gradient(
+ circle at 22% 28%,
+ rgba(255, 196, 130, 0.85),
+ rgba(255, 196, 130, 0) 38%
+ ),
+ radial-gradient(circle at 78% 78%, rgba(64, 175, 200, 0.55), rgba(64, 175, 200, 0) 42%),
+ radial-gradient(circle at 50% 50%, #3b2616 0%, #1a0e07 68%, #060403 100%);
+ position: relative;
+ overflow: hidden;
+ font-family: "Inter", "Helvetica Neue", Helvetica, Arial, sans-serif;
+ }
+
+ .demo-haze {
+ position: absolute;
+ left: 50%;
+ top: 50%;
+ border-radius: 50%;
+ transform: translate(-50%, -50%) scale(1);
+ pointer-events: none;
+ opacity: 0;
+ }
+ .haze-far {
+ width: 1400px;
+ height: 1400px;
+ background: radial-gradient(
+ circle,
+ rgba(255, 220, 180, 0.18),
+ rgba(255, 220, 180, 0) 65%
+ );
+ filter: blur(40px);
+ }
+ .haze-near {
+ width: 900px;
+ height: 900px;
+ background: radial-gradient(
+ circle,
+ rgba(255, 240, 210, 0.32),
+ rgba(255, 240, 210, 0) 60%
+ );
+ filter: blur(20px);
+ }
+
+ .demo-subject {
+ position: absolute;
+ left: 50%;
+ top: 50%;
+ width: 520px;
+ height: 520px;
+ border-radius: 50%;
+ transform: translate(-50%, -50%);
+ background: radial-gradient(
+ circle at 35% 32%,
+ #fff6e0 0%,
+ #f4c47e 28%,
+ #b87a3f 62%,
+ #6b3d1c 88%,
+ #2a1607 100%
+ );
+ box-shadow:
+ inset -40px -40px 80px rgba(0, 0, 0, 0.55),
+ 0 0 120px rgba(255, 180, 100, 0.35);
+ opacity: 0;
+ }
+
+ .demo-text {
+ position: absolute;
+ left: 0;
+ right: 0;
+ bottom: 12%;
display: flex;
flex-direction: column;
- justify-content: center;
align-items: center;
- font-family: "Inter", sans-serif;
- position: relative;
+ gap: 14px;
+ pointer-events: none;
+ /* Sits above the subject in document order; below the vignette in z-index */
+ z-index: 5;
+ }
+
+ .demo-title {
+ font-size: 108px;
+ font-weight: 800;
+ letter-spacing: 0.18em;
+ color: #f6ecd0;
+ text-shadow: 0 4px 24px rgba(0, 0, 0, 0.55);
+ opacity: 0;
+ }
+
+ .demo-subtitle {
+ font-size: 30px;
+ font-weight: 400;
+ letter-spacing: 0.32em;
+ text-transform: uppercase;
+ color: rgba(246, 236, 208, 0.72);
+ opacity: 0;
}
#hf-vignette {
@@ -89,32 +174,53 @@
(function () {
const tl = gsap.timeline({ paused: true });
- // Fade in title and subtitle
- tl.to(".demo-title", { opacity: 1, y: -10, duration: 1, ease: "power3.out" }, 0.3);
- tl.to(".demo-subtitle", { opacity: 1, y: -5, duration: 0.8, ease: "power3.out" }, 0.7);
+ // Reveal the backdrop subject and haze first so the vignette has
+ // something to frame. Without a subject the effect reads as a flat
+ // dark gradient — uninteresting in a still preview.
+ tl.to(".haze-far", { opacity: 1, duration: 0.8, ease: "power2.out" }, 0.0);
+ tl.to(".haze-near", { opacity: 1, duration: 0.8, ease: "power2.out" }, 0.15);
+ tl.to(".demo-subject", { opacity: 1, scale: 1, duration: 1.0, ease: "power3.out" }, 0.1);
- // Fade vignette in from transparent
+ // Title + subtitle settle in.
tl.fromTo(
+ ".demo-title",
+ { opacity: 0, y: 18, letterSpacing: "0.32em" },
+ { opacity: 1, y: 0, letterSpacing: "0.18em", duration: 1.0, ease: "power3.out" },
+ 0.6,
+ );
+ tl.to(".demo-subtitle", { opacity: 1, duration: 0.8, ease: "power3.out" }, 1.0);
+
+ // Vignette: start visibly soft (so viewers see the unframed scene),
+ // then ramp to a strong cinematic vignette and hold through the
+ // capture-time peak (~3.0s, picked to match the catalog thumbnail
+ // sampling at `Math.min(3.0, duration * 0.6)` for duration=5).
+ gsap.set("#hf-vignette", {
+ "--vignette-color": "rgba(0, 0, 0, 0.35)",
+ "--vignette-size": "70%",
+ "--vignette-edge": "100%",
+ });
+ tl.to(
"#hf-vignette",
- { "--vignette-color": "rgba(0, 0, 0, 0)" },
{
- "--vignette-color": "rgba(0, 0, 0, 0.75)",
- duration: 1.5,
- ease: "power2.out",
+ "--vignette-color": "rgba(0, 0, 0, 0.92)",
+ "--vignette-size": "26%",
+ duration: 1.6,
+ ease: "power2.inOut",
},
- 1.2,
+ 1.4,
);
- // Breathe the size in and out
+ // Gentle breathing for the video loop so the effect reads as alive,
+ // not static. Keeps the still-frame at peak intensity around t≈3s.
tl.to(
"#hf-vignette",
- { "--vignette-size": "35%", duration: 1.0, ease: "sine.inOut" },
- 3.0,
+ { "--vignette-size": "32%", duration: 0.9, ease: "sine.inOut" },
+ 3.4,
);
tl.to(
"#hf-vignette",
- { "--vignette-size": "45%", duration: 1.0, ease: "sine.inOut" },
- 4.0,
+ { "--vignette-size": "26%", duration: 0.9, ease: "sine.inOut" },
+ 4.3,
);
window.__timelines = window.__timelines || {};