feat(registry): add parallax-zoom and parallax-unzoom components

Two companion components inspired by the eBay Playbook hero transition:

- parallax-zoom: center card scales up to fill the frame while siblings
  parallax outward. Single CSS variable (--pz-progress 0→1), fully
  seekable and deterministic.

- parallax-unzoom: the reverse — focus card starts at full-frame scale
  and shrinks back into its grid position while siblings parallax inward.
  Uses --pu-progress with the pu prefix to avoid variable collisions when
  both components live in the same composition.

Designed to chain: zoom INTO a card in scene 1, unzoom OUT of it in
scene 2 to reveal a fresh grid underneath.

Includes demo compositions, registry manifests, and catalog pages.
Preview assets rendered and uploaded to CDN.

Co-authored-by: Kanyini <onebenson@gmail.com>
This commit is contained in:
Miguel Ángel
2026-05-22 18:42:50 -04:00
co-authored by Kanyini
parent 154359d95d
commit babf9dc15a
9 changed files with 978 additions and 0 deletions
@@ -0,0 +1,38 @@
---
title: "Parallax Unzoom"
description: "Reveal transition — focus card scales down from full frame as siblings parallax in to form a grid (reverse of parallax-zoom)"
---
# Parallax Unzoom
Reveal transition — focus card scales down from full frame as siblings parallax in to form a grid (reverse of parallax-zoom)
`transition` `reveal` `unzoom` `parallax` `grid` `hero`
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/parallax-unzoom.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/parallax-unzoom.png" autoPlay muted loop playsInline />
## Install
<CodeGroup>
```bash Terminal
npx hyperframes add parallax-unzoom
```
</CodeGroup>
## Details
| Property | Value |
| --- | --- |
| Type | Component |
## Files
| File | Target | Type |
| --- | --- | --- |
| `parallax-unzoom.html` | `compositions/components/parallax-unzoom.html` | hyperframes:snippet |
## Usage
Open `compositions/components/parallax-unzoom.html` and paste its contents into your composition. See the comment header in the file for detailed instructions.
+38
View File
@@ -0,0 +1,38 @@
---
title: "Parallax Zoom"
description: "Center card scales up to fill the frame while siblings parallax outward — inspired by the eBay Playbook hero transition"
---
# Parallax Zoom
Center card scales up to fill the frame while siblings parallax outward — inspired by the eBay Playbook hero transition
`transition` `zoom` `parallax` `grid` `hero`
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/parallax-zoom.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/parallax-zoom.png" autoPlay muted loop playsInline />
## Install
<CodeGroup>
```bash Terminal
npx hyperframes add parallax-zoom
```
</CodeGroup>
## Details
| Property | Value |
| --- | --- |
| Type | Component |
## Files
| File | Target | Type |
| --- | --- | --- |
| `parallax-zoom.html` | `compositions/components/parallax-zoom.html` | hyperframes:snippet |
## Usage
Open `compositions/components/parallax-zoom.html` and paste its contents into your composition. See the comment header in the file for detailed instructions.
+317
View File
@@ -0,0 +1,317 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=1920, height=1080" />
<title>Parallax Unzoom — Demo</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800;900&display=swap"
rel="stylesheet"
/>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
width: 1920px;
height: 1080px;
overflow: hidden;
background: #f6f6f4;
font-family: "Inter", system-ui, sans-serif;
}
.demo-canvas {
width: 1920px;
height: 1080px;
position: relative;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
}
.parallax-unzoom-grid {
display: grid;
grid-template-columns: repeat(5, 240px);
grid-template-rows: repeat(3, 320px);
gap: 40px;
}
.parallax-unzoom-card {
border-radius: 22px;
overflow: hidden;
position: relative;
box-shadow: 0 12px 36px rgba(20, 20, 30, 0.08);
display: flex;
align-items: center;
justify-content: center;
}
.card-glyph {
font-size: 92px;
font-weight: 800;
color: rgba(255, 255, 255, 0.92);
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.18);
letter-spacing: -0.04em;
line-height: 1;
}
.pu-c0 {
background: linear-gradient(135deg, #3a8b6f, #1d4d3d);
}
.pu-c1 {
background: linear-gradient(135deg, #8b4a3a, #4a2418);
}
.pu-c2 {
background: linear-gradient(135deg, #c89868, #8a6038);
}
.pu-c3 {
background: linear-gradient(135deg, #3a6088, #1a2f4a);
}
.pu-c4 {
background: linear-gradient(135deg, #b03838, #6a1818);
}
.pu-c5 {
background: linear-gradient(135deg, #bcbcbc, #6e6e6e);
}
.pu-c6 {
background: linear-gradient(135deg, #8a5fa0, #4a2860);
}
.pu-c7-focus {
background: linear-gradient(135deg, #f5c93f, #d99a18);
}
.pu-c8 {
background: linear-gradient(135deg, #d8d8d8, #9a9a9a);
}
.pu-c9 {
background: linear-gradient(135deg, #5fa37e, #2a6a4a);
}
.pu-c10 {
background: linear-gradient(135deg, #d1b66a, #8a7028);
}
.pu-c11 {
background: linear-gradient(135deg, #4f7fc0, #1f3f80);
}
.pu-c12 {
background: linear-gradient(135deg, #5a3a8f, #2a1858);
}
.pu-c13 {
background: linear-gradient(135deg, #b85a3a, #6a2818);
}
.pu-c14 {
background: linear-gradient(135deg, #f0f0e8, #c8c8be);
}
.parallax-unzoom-card[data-pu-focus="true"] {
grid-column: 3;
grid-row: 2;
}
.focus-label {
font-weight: 900;
font-size: 56px;
letter-spacing: -0.04em;
color: #1a1208;
text-align: center;
line-height: 1;
opacity: 0;
}
.focus-label small {
display: block;
font-size: 18px;
font-weight: 600;
letter-spacing: 0.18em;
margin-bottom: 14px;
color: rgba(26, 18, 8, 0.7);
}
.hero-headline {
position: absolute;
top: 80px;
left: 0;
width: 100%;
text-align: center;
font-size: 68px;
font-weight: 800;
letter-spacing: -0.03em;
color: #1a1a1a;
opacity: 1;
}
.parallax-unzoom-grid {
--pu-progress: 0;
--pu-focus-scale: 8;
--pu-sibling-push: 900px;
--pu-sibling-fade: 1;
position: relative;
transform-style: preserve-3d;
}
.parallax-unzoom-card {
transform-origin: center center;
will-change: transform, opacity;
transform: translate3d(
calc(var(--pu-dx, 0px) * (1 - var(--pu-progress))),
calc(var(--pu-dy, 0px) * (1 - var(--pu-progress))),
0
)
scale(calc(0.7 + 0.3 * var(--pu-progress)));
opacity: calc(var(--pu-progress) * var(--pu-sibling-fade));
}
.parallax-unzoom-card[data-pu-focus="true"] {
z-index: 10;
transform: translate3d(0, 0, 0)
scale(calc(var(--pu-focus-scale) - (var(--pu-focus-scale) - 1) * var(--pu-progress)));
opacity: 1;
}
</style>
</head>
<body>
<div
id="demo"
data-composition-id="parallax-unzoom-demo"
data-width="1920"
data-height="1080"
data-duration="6"
data-start="0"
>
<div class="demo-canvas">
<div class="hero-headline">Inspired by how people discover.</div>
<div class="parallax-unzoom-grid">
<div class="parallax-unzoom-card pu-c0" data-pu-row="0" data-pu-col="0">
<span class="card-glyph"></span>
</div>
<div class="parallax-unzoom-card pu-c1" data-pu-row="0" data-pu-col="1">
<span class="card-glyph"></span>
</div>
<div class="parallax-unzoom-card pu-c2" data-pu-row="0" data-pu-col="2">
<span class="card-glyph"></span>
</div>
<div class="parallax-unzoom-card pu-c3" data-pu-row="0" data-pu-col="3">
<span class="card-glyph"></span>
</div>
<div class="parallax-unzoom-card pu-c4" data-pu-row="0" data-pu-col="4">
<span class="card-glyph"></span>
</div>
<div class="parallax-unzoom-card pu-c5" data-pu-row="1" data-pu-col="0">
<span class="card-glyph"></span>
</div>
<div class="parallax-unzoom-card pu-c6" data-pu-row="1" data-pu-col="1">
<span class="card-glyph"></span>
</div>
<div
class="parallax-unzoom-card pu-c7-focus"
data-pu-row="1"
data-pu-col="2"
data-pu-focus="true"
>
<div class="focus-label">
<small>EBAY PLAYBOOK</small>
DISCOVER
</div>
</div>
<div class="parallax-unzoom-card pu-c8" data-pu-row="1" data-pu-col="3">
<span class="card-glyph"></span>
</div>
<div class="parallax-unzoom-card pu-c9" data-pu-row="1" data-pu-col="4">
<span class="card-glyph"></span>
</div>
<div class="parallax-unzoom-card pu-c10" data-pu-row="2" data-pu-col="0">
<span class="card-glyph"></span>
</div>
<div class="parallax-unzoom-card pu-c11" data-pu-row="2" data-pu-col="1">
<span class="card-glyph"></span>
</div>
<div class="parallax-unzoom-card pu-c12" data-pu-row="2" data-pu-col="2">
<span class="card-glyph"></span>
</div>
<div class="parallax-unzoom-card pu-c13" data-pu-row="2" data-pu-col="3">
<span class="card-glyph"></span>
</div>
<div class="parallax-unzoom-card pu-c14" data-pu-row="2" data-pu-col="4">
<span class="card-glyph" style="color: rgba(40, 40, 40, 0.7)"></span>
</div>
</div>
</div>
<script>
(function () {
const grids = document.querySelectorAll(".parallax-unzoom-grid");
grids.forEach((grid) => {
const cards = Array.from(grid.querySelectorAll(".parallax-unzoom-card"));
if (cards.length === 0) return;
let focal = cards.find((c) => c.dataset.puFocus === "true");
if (!focal) {
focal = cards[Math.floor(cards.length / 2)];
focal.dataset.puFocus = "true";
}
const focusRow = Number(focal.dataset.puRow ?? 0);
const focusCol = Number(focal.dataset.puCol ?? 0);
const push =
parseFloat(getComputedStyle(grid).getPropertyValue("--pu-sibling-push")) || 900;
cards.forEach((card) => {
if (card === focal) return;
const row = Number(card.dataset.puRow ?? 0);
const col = Number(card.dataset.puCol ?? 0);
const dCol = col - focusCol;
const dRow = row - focusRow;
const mag = Math.hypot(dCol, dRow) || 1;
const nx = (dCol / mag) * push * mag;
const ny = (dRow / mag) * push * mag;
card.style.setProperty("--pu-dx", `${nx}px`);
card.style.setProperty("--pu-dy", `${ny}px`);
});
});
const tl = gsap.timeline({ paused: true });
tl.to(
".hero-headline",
{
opacity: 0,
duration: 0.5,
ease: "power2.in",
},
0.3,
);
tl.fromTo(
".parallax-unzoom-grid",
{ "--pu-progress": 0 },
{
"--pu-progress": 1,
duration: 3.0,
ease: "power2.inOut",
},
0.5,
);
tl.to(
".focus-label",
{
opacity: 1,
duration: 1.0,
ease: "power2.out",
},
2.5,
);
window.__timelines = window.__timelines || {};
window.__timelines["parallax-unzoom-demo"] = tl;
})();
</script>
</div>
</body>
</html>
+104
View File
@@ -0,0 +1,104 @@
<!--
Parallax Unzoom — focus card scales down from full frame while siblings
parallax IN to form a grid. The reverse of parallax-zoom.
Pairs naturally with parallax-zoom: zoom INTO a card in scene 1, then
unzoom OUT of it in scene 2 to reveal a fresh grid underneath.
Usage:
1. Wrap your card grid with class="parallax-unzoom-grid".
2. Mark each card with class="parallax-unzoom-card".
3. Add data-pu-row="0|1|2|..." and data-pu-col="0|1|2|..." per card.
4. Mark the card that starts at full size with data-pu-focus="true".
5. Drive --pu-progress 0 → 1 on the .parallax-unzoom-grid from your
composition's paused GSAP timeline (see example at bottom).
Tunable CSS variables on .parallax-unzoom-grid:
- --pu-focus-scale (default 6) starting scale of the focus card
- --pu-sibling-push (default 900px) outward distance siblings start at
- --pu-sibling-fade (default 1) sibling opacity falloff (01)
-->
<style>
.parallax-unzoom-grid {
--pu-progress: 0;
--pu-focus-scale: 6;
--pu-sibling-push: 900px;
--pu-sibling-fade: 1;
position: relative;
transform-style: preserve-3d;
}
.parallax-unzoom-card {
transform-origin: center center;
will-change: transform, opacity;
transform: translate3d(
calc(var(--pu-dx, 0px) * (1 - var(--pu-progress))),
calc(var(--pu-dy, 0px) * (1 - var(--pu-progress))),
0
)
scale(calc(0.7 + 0.3 * var(--pu-progress)));
opacity: calc(var(--pu-progress) * var(--pu-sibling-fade));
}
.parallax-unzoom-card[data-pu-focus="true"] {
z-index: 10;
transform: translate3d(0, 0, 0)
scale(calc(var(--pu-focus-scale) - (var(--pu-focus-scale) - 1) * var(--pu-progress)));
opacity: 1;
}
</style>
<script>
(function () {
const grids = document.querySelectorAll(".parallax-unzoom-grid");
grids.forEach((grid) => {
const cards = Array.from(grid.querySelectorAll(".parallax-unzoom-card"));
if (cards.length === 0) return;
let focal = cards.find((c) => c.dataset.puFocus === "true");
if (!focal) {
focal = cards[Math.floor(cards.length / 2)];
focal.dataset.puFocus = "true";
}
const focusRow = Number(focal.dataset.puRow ?? 0);
const focusCol = Number(focal.dataset.puCol ?? 0);
const push = parseFloat(getComputedStyle(grid).getPropertyValue("--pu-sibling-push")) || 900;
cards.forEach((card) => {
if (card === focal) return;
const row = Number(card.dataset.puRow ?? 0);
const col = Number(card.dataset.puCol ?? 0);
const dCol = col - focusCol;
const dRow = row - focusRow;
const mag = Math.hypot(dCol, dRow) || 1;
const nx = (dCol / mag) * push * mag;
const ny = (dRow / mag) * push * mag;
card.style.setProperty("--pu-dx", `${nx}px`);
card.style.setProperty("--pu-dy", `${ny}px`);
});
});
})();
</script>
<!--
Timeline integration example (paste into your composition's GSAP timeline):
tl.fromTo(
".parallax-unzoom-grid",
{ "--pu-progress": 0 },
{
"--pu-progress": 1,
duration: 2.4,
ease: "power2.inOut",
},
startTime, // start time in seconds
);
Chaining with parallax-zoom example:
// Scene 1 (0-3s): zoom IN to the focal card via parallax-zoom
// Scene 2 (3-6s): unzoom OUT of a NEW grid via parallax-unzoom,
// with the focal card pre-positioned to match scene 1
-->
@@ -0,0 +1,18 @@
{
"$schema": "https://hyperframes.heygen.com/schema/registry-item.json",
"name": "parallax-unzoom",
"type": "hyperframes:component",
"title": "Parallax Unzoom",
"description": "Reveal transition — focus card scales down from full frame as siblings parallax in to form a grid (reverse of parallax-zoom)",
"tags": ["transition", "reveal", "unzoom", "parallax", "grid", "hero"],
"files": [
{
"path": "parallax-unzoom.html",
"target": "compositions/components/parallax-unzoom.html",
"type": "hyperframes:snippet"
}
],
"preview": {
"poster": "https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/parallax-unzoom.png"
}
}
+337
View File
@@ -0,0 +1,337 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=1920, height=1080" />
<title>Parallax Zoom — Demo</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800;900&display=swap"
rel="stylesheet"
/>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
width: 1920px;
height: 1080px;
overflow: hidden;
background: #f6f6f4;
font-family: "Inter", system-ui, sans-serif;
}
.demo-canvas {
width: 1920px;
height: 1080px;
position: relative;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
}
.parallax-zoom-grid {
display: grid;
grid-template-columns: repeat(5, 240px);
grid-template-rows: repeat(3, 320px);
gap: 40px;
}
.parallax-zoom-card {
border-radius: 22px;
overflow: hidden;
position: relative;
box-shadow: 0 12px 36px rgba(20, 20, 30, 0.08);
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
}
.card-glyph {
font-size: 92px;
font-weight: 800;
color: rgba(255, 255, 255, 0.92);
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.18);
letter-spacing: -0.04em;
line-height: 1;
}
.pz-c0 {
background: linear-gradient(135deg, #3a8b6f, #1d4d3d);
}
.pz-c1 {
background: linear-gradient(135deg, #8b4a3a, #4a2418);
}
.pz-c2 {
background: linear-gradient(135deg, #c89868, #8a6038);
}
.pz-c3 {
background: linear-gradient(135deg, #3a6088, #1a2f4a);
}
.pz-c4 {
background: linear-gradient(135deg, #b03838, #6a1818);
}
.pz-c5 {
background: linear-gradient(135deg, #bcbcbc, #6e6e6e);
}
.pz-c6 {
background: linear-gradient(135deg, #8a5fa0, #4a2860);
}
.pz-c7-focus {
background: linear-gradient(135deg, #f5c93f, #d99a18);
}
.pz-c8 {
background: linear-gradient(135deg, #d8d8d8, #9a9a9a);
}
.pz-c9 {
background: linear-gradient(135deg, #5fa37e, #2a6a4a);
}
.pz-c10 {
background: linear-gradient(135deg, #d1b66a, #8a7028);
}
.pz-c11 {
background: linear-gradient(135deg, #4f7fc0, #1f3f80);
}
.pz-c12 {
background: linear-gradient(135deg, #5a3a8f, #2a1858);
}
.pz-c13 {
background: linear-gradient(135deg, #b85a3a, #6a2818);
}
.pz-c14 {
background: linear-gradient(135deg, #f0f0e8, #c8c8be);
}
.parallax-zoom-card[data-pz-focus="true"] {
grid-column: 3;
grid-row: 2;
}
.focus-label {
font-weight: 900;
font-size: 56px;
letter-spacing: -0.04em;
color: #1a1208;
text-align: center;
line-height: 1;
}
.focus-label small {
display: block;
font-size: 18px;
font-weight: 600;
letter-spacing: 0.18em;
margin-bottom: 14px;
color: rgba(26, 18, 8, 0.7);
}
.hero-headline {
position: absolute;
top: 80px;
left: 0;
width: 100%;
text-align: center;
font-size: 68px;
font-weight: 800;
letter-spacing: -0.03em;
color: #1a1a1a;
opacity: 0;
z-index: 20;
}
.parallax-zoom-grid {
--pz-progress: 0;
--pz-focus-scale: 8;
--pz-sibling-push: 900px;
--pz-sibling-fade: 1;
position: relative;
transform-style: preserve-3d;
}
.parallax-zoom-card {
transform-origin: center center;
will-change: transform, opacity;
transform: translate3d(
calc(var(--pz-dx, 0px) * var(--pz-progress)),
calc(var(--pz-dy, 0px) * var(--pz-progress)),
0
)
scale(calc(1 - var(--pz-progress) * 0.4));
}
.parallax-zoom-card[data-pz-focus="true"] {
z-index: 10;
transform: translate3d(0, 0, 0)
scale(calc(1 + var(--pz-progress) * (var(--pz-focus-scale) - 1)));
}
</style>
</head>
<body>
<div
id="demo"
data-composition-id="parallax-zoom-demo"
data-width="1920"
data-height="1080"
data-duration="6"
data-start="0"
>
<div class="demo-canvas">
<div class="hero-headline">Inspired by how people discover.</div>
<div class="parallax-zoom-grid">
<div class="parallax-zoom-card pz-c0" data-pz-row="0" data-pz-col="0">
<span class="card-glyph"></span>
</div>
<div class="parallax-zoom-card pz-c1" data-pz-row="0" data-pz-col="1">
<span class="card-glyph"></span>
</div>
<div class="parallax-zoom-card pz-c2" data-pz-row="0" data-pz-col="2">
<span class="card-glyph"></span>
</div>
<div class="parallax-zoom-card pz-c3" data-pz-row="0" data-pz-col="3">
<span class="card-glyph"></span>
</div>
<div class="parallax-zoom-card pz-c4" data-pz-row="0" data-pz-col="4">
<span class="card-glyph"></span>
</div>
<div class="parallax-zoom-card pz-c5" data-pz-row="1" data-pz-col="0">
<span class="card-glyph"></span>
</div>
<div class="parallax-zoom-card pz-c6" data-pz-row="1" data-pz-col="1">
<span class="card-glyph"></span>
</div>
<div
class="parallax-zoom-card pz-c7-focus"
data-pz-row="1"
data-pz-col="2"
data-pz-focus="true"
>
<div class="focus-label">
<small>EBAY PLAYBOOK</small>
DISCOVER
</div>
</div>
<div class="parallax-zoom-card pz-c8" data-pz-row="1" data-pz-col="3">
<span class="card-glyph"></span>
</div>
<div class="parallax-zoom-card pz-c9" data-pz-row="1" data-pz-col="4">
<span class="card-glyph"></span>
</div>
<div class="parallax-zoom-card pz-c10" data-pz-row="2" data-pz-col="0">
<span class="card-glyph"></span>
</div>
<div class="parallax-zoom-card pz-c11" data-pz-row="2" data-pz-col="1">
<span class="card-glyph"></span>
</div>
<div class="parallax-zoom-card pz-c12" data-pz-row="2" data-pz-col="2">
<span class="card-glyph"></span>
</div>
<div class="parallax-zoom-card pz-c13" data-pz-row="2" data-pz-col="3">
<span class="card-glyph"></span>
</div>
<div class="parallax-zoom-card pz-c14" data-pz-row="2" data-pz-col="4">
<span class="card-glyph" style="color: rgba(40, 40, 40, 0.7)"></span>
</div>
</div>
</div>
<script>
(function () {
const grids = document.querySelectorAll(".parallax-zoom-grid");
grids.forEach((grid) => {
const cards = Array.from(grid.querySelectorAll(".parallax-zoom-card"));
if (cards.length === 0) return;
let focal = cards.find((c) => c.dataset.pzFocus === "true");
if (!focal) {
focal = cards[Math.floor(cards.length / 2)];
focal.dataset.pzFocus = "true";
}
const focusRow = Number(focal.dataset.pzRow ?? 0);
const focusCol = Number(focal.dataset.pzCol ?? 0);
const push =
parseFloat(getComputedStyle(grid).getPropertyValue("--pz-sibling-push")) || 900;
cards.forEach((card) => {
if (card === focal) return;
const row = Number(card.dataset.pzRow ?? 0);
const col = Number(card.dataset.pzCol ?? 0);
const dCol = col - focusCol;
const dRow = row - focusRow;
const mag = Math.hypot(dCol, dRow) || 1;
const nx = (dCol / mag) * push * mag;
const ny = (dRow / mag) * push * mag;
card.style.setProperty("--pz-dx", `${nx}px`);
card.style.setProperty("--pz-dy", `${ny}px`);
});
});
const tl = gsap.timeline({ paused: true });
tl.to(
".parallax-zoom-card",
{
opacity: 1,
duration: 0.5,
ease: "power2.out",
stagger: { each: 0.03, from: "center" },
},
0,
);
tl.fromTo(
".parallax-zoom-grid",
{ "--pz-progress": 0 },
{
"--pz-progress": 1,
duration: 3.0,
ease: "power2.inOut",
},
1.7,
);
tl.to(
".parallax-zoom-card:not([data-pz-focus='true'])",
{
opacity: 0,
duration: 1.0,
ease: "power2.in",
},
3.5,
);
tl.to(
".focus-label",
{
opacity: 0,
duration: 1.0,
ease: "power2.in",
},
2.5,
);
tl.to(
".hero-headline",
{
opacity: 1,
duration: 0.7,
ease: "power2.out",
},
4.2,
);
window.__timelines = window.__timelines || {};
window.__timelines["parallax-zoom-demo"] = tl;
})();
</script>
</div>
</body>
</html>
+100
View File
@@ -0,0 +1,100 @@
<!--
Parallax Zoom — center card scales up while siblings parallax outward.
Inspired by the eBay Playbook hero transition: a grid of cards collapses
as the focal card grows to fill the frame.
Usage:
1. Wrap your card grid with class="parallax-zoom-grid".
2. Mark each card with class="parallax-zoom-card".
3. Add data-pz-row="0|1|2|..." and data-pz-col="0|1|2|..." per card
so the snippet knows each card's grid offset.
4. Mark the focal card with data-pz-focus="true" — this is the one
that scales up to fill the frame.
5. Drive --pz-progress 0 → 1 on the .parallax-zoom-grid from your
composition's paused GSAP timeline (see example at bottom).
Tunable CSS variables on .parallax-zoom-grid:
- --pz-focus-scale (default 6) maximum scale of the focused card
- --pz-sibling-push (default 900px) outward translation strength
- --pz-sibling-fade (default 1) sibling opacity falloff (01)
-->
<style>
.parallax-zoom-grid {
--pz-progress: 0;
--pz-focus-scale: 6;
--pz-sibling-push: 900px;
--pz-sibling-fade: 1;
position: relative;
transform-style: preserve-3d;
}
.parallax-zoom-card {
transform-origin: center center;
will-change: transform, opacity;
transform: translate3d(
calc(var(--pz-dx, 0px) * var(--pz-progress)),
calc(var(--pz-dy, 0px) * var(--pz-progress)),
0
)
scale(calc(1 - var(--pz-progress) * 0.4));
opacity: calc(1 - var(--pz-progress) * var(--pz-sibling-fade));
}
.parallax-zoom-card[data-pz-focus="true"] {
z-index: 10;
transform: translate3d(0, 0, 0)
scale(calc(1 + var(--pz-progress) * (var(--pz-focus-scale) - 1)));
opacity: 1;
}
</style>
<script>
(function () {
const grids = document.querySelectorAll(".parallax-zoom-grid");
grids.forEach((grid) => {
const cards = Array.from(grid.querySelectorAll(".parallax-zoom-card"));
if (cards.length === 0) return;
let focal = cards.find((c) => c.dataset.pzFocus === "true");
if (!focal) {
focal = cards[Math.floor(cards.length / 2)];
focal.dataset.pzFocus = "true";
}
const focusRow = Number(focal.dataset.pzRow ?? 0);
const focusCol = Number(focal.dataset.pzCol ?? 0);
const push = parseFloat(getComputedStyle(grid).getPropertyValue("--pz-sibling-push")) || 900;
cards.forEach((card) => {
if (card === focal) return;
const row = Number(card.dataset.pzRow ?? 0);
const col = Number(card.dataset.pzCol ?? 0);
const dCol = col - focusCol;
const dRow = row - focusRow;
const mag = Math.hypot(dCol, dRow) || 1;
const nx = (dCol / mag) * push * mag;
const ny = (dRow / mag) * push * mag;
card.style.setProperty("--pz-dx", `${nx}px`);
card.style.setProperty("--pz-dy", `${ny}px`);
});
});
})();
</script>
<!--
Timeline integration example (paste into your composition's GSAP timeline):
tl.fromTo(
".parallax-zoom-grid",
{ "--pz-progress": 0 },
{
"--pz-progress": 1,
duration: 2.4,
ease: "power2.inOut",
},
1.0, // start time in seconds
);
-->
@@ -0,0 +1,18 @@
{
"$schema": "https://hyperframes.heygen.com/schema/registry-item.json",
"name": "parallax-zoom",
"type": "hyperframes:component",
"title": "Parallax Zoom",
"description": "Center card scales up to fill the frame while siblings parallax outward — inspired by the eBay Playbook hero transition",
"tags": ["transition", "zoom", "parallax", "grid", "hero"],
"files": [
{
"path": "parallax-zoom.html",
"target": "compositions/components/parallax-zoom.html",
"type": "hyperframes:snippet"
}
],
"preview": {
"poster": "https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/parallax-zoom.png"
}
}
+8
View File
@@ -346,6 +346,14 @@
{
"name": "vfx-shatter",
"type": "hyperframes:block"
},
{
"name": "parallax-zoom",
"type": "hyperframes:component"
},
{
"name": "parallax-unzoom",
"type": "hyperframes:component"
}
]
}