Skip to content

Commit c329b25

Browse files
committed
1,639th Commit
1 parent f653b74 commit c329b25

1 file changed

Lines changed: 328 additions & 0 deletions

File tree

  • app/src/routes/(landing)/stacking-cards
Lines changed: 328 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,328 @@
1+
<script lang="ts" setup>
2+
import { ref, onMounted, onUnmounted } from "vue";
3+
4+
const stackCardsRef = ref<HTMLElement | null>(null);
5+
let observer: IntersectionObserver | null = null;
6+
let scrollingFn: (() => void) | null = null;
7+
let scrolling = false;
8+
9+
// Card properties
10+
let myMarginY = 0;
11+
let myElementHeight = 0;
12+
let myCardTop = 0;
13+
let myCardHeight = 0;
14+
let myWindowHeight = 0;
15+
16+
function osHasReducedMotion() {
17+
if (!window.matchMedia) return false;
18+
const matchMediaObj = window.matchMedia("(prefers-reduced-motion: reduce)");
19+
if (matchMediaObj) return matchMediaObj.matches;
20+
return false;
21+
}
22+
23+
function setStackCards() {
24+
const element = stackCardsRef.value;
25+
if (!element) return;
26+
27+
const items = element.children as HTMLCollectionOf<HTMLElement>;
28+
29+
// store wrapper properties
30+
const computedStyle = getComputedStyle(element);
31+
const marginYProp = computedStyle.getPropertyValue("--stack-cards-gap");
32+
33+
// Create a temporary element to resolve the var() if needed, or just parse if it's pixels
34+
// The original code used a div to compute it. Let's try to parse directly or duplicate the trick if needed.
35+
// Actually, getComputedStyle on the element should resolve CSS variables if we access the property.
36+
// However, the original code had a specific 'getIntegerFromProperty' using a fake div.
37+
// Let's stick to a simpler approach first: check if it returns a px value.
38+
39+
// Replicating the original trick to be safe about unit conversion (rem to px etc)
40+
const node = document.createElement("div");
41+
node.style.opacity = "0";
42+
node.style.visibility = "hidden";
43+
node.style.position = "absolute";
44+
node.style.height = marginYProp;
45+
element.appendChild(node);
46+
myMarginY = parseInt(getComputedStyle(node).height);
47+
element.removeChild(node);
48+
49+
myElementHeight = element.offsetHeight;
50+
51+
if (items.length > 0) {
52+
const cardStyle = getComputedStyle(items[0]);
53+
myCardTop = Math.floor(parseFloat(cardStyle.top));
54+
// Use offsetHeight to capture the full visual height including padding/borders
55+
myCardHeight = items[0].offsetHeight;
56+
}
57+
58+
myWindowHeight = window.innerHeight;
59+
60+
// reset margin + translate values
61+
// original: element.element.style.paddingBottom = ...
62+
if (isNaN(myMarginY)) {
63+
element.style.paddingBottom = "0px";
64+
} else {
65+
element.style.paddingBottom = `${myMarginY * (items.length - 1)}px`;
66+
}
67+
68+
for (let i = 0; i < items.length; i++) {
69+
if (isNaN(myMarginY)) {
70+
items[i].style.transform = "none";
71+
} else {
72+
items[i].style.transform = `translateY(${myMarginY * i}px)`;
73+
}
74+
}
75+
}
76+
77+
function animateStackCards() {
78+
const element = stackCardsRef.value;
79+
if (!element) return;
80+
81+
if (isNaN(myMarginY)) {
82+
scrolling = false;
83+
return;
84+
}
85+
86+
const items = element.children as HTMLCollectionOf<HTMLElement>;
87+
const top = element.getBoundingClientRect().top;
88+
89+
if (
90+
myCardTop -
91+
top +
92+
myWindowHeight -
93+
myElementHeight -
94+
myCardHeight +
95+
myMarginY +
96+
myMarginY * items.length >
97+
0
98+
) {
99+
scrolling = false;
100+
return;
101+
}
102+
103+
for (let i = 0; i < items.length; i++) {
104+
const scrollingVal = myCardTop - top - i * (myCardHeight + myMarginY);
105+
if (scrollingVal > 0) {
106+
const scaling =
107+
i === items.length - 1
108+
? 1
109+
: (myCardHeight - scrollingVal * 0.05) / myCardHeight;
110+
items[i].style.transform = `translateY(${
111+
myMarginY * i
112+
}px) scale(${scaling})`;
113+
} else {
114+
items[i].style.transform = `translateY(${myMarginY * i}px)`;
115+
}
116+
}
117+
118+
scrolling = false;
119+
}
120+
121+
function stackCardsScrolling() {
122+
if (scrolling) return;
123+
scrolling = true;
124+
window.requestAnimationFrame(animateStackCards);
125+
}
126+
127+
function stackCardsCallback(entries: IntersectionObserverEntry[]) {
128+
if (entries[0].isIntersecting) {
129+
if (scrollingFn) return;
130+
scrollingFn = stackCardsScrolling;
131+
window.addEventListener("scroll", scrollingFn);
132+
} else {
133+
if (!scrollingFn) return;
134+
window.removeEventListener("scroll", scrollingFn);
135+
scrollingFn = null;
136+
}
137+
}
138+
139+
let resizeId: ReturnType<typeof setTimeout>;
140+
141+
function handleResize() {
142+
clearTimeout(resizeId);
143+
resizeId = setTimeout(() => {
144+
setStackCards();
145+
// Force an animation frame update
146+
scrolling = false;
147+
animateStackCards();
148+
}, 500);
149+
}
150+
151+
onMounted(() => {
152+
const element = stackCardsRef.value;
153+
if (element && "IntersectionObserver" in window && !osHasReducedMotion()) {
154+
setStackCards();
155+
observer = new IntersectionObserver(stackCardsCallback, {
156+
threshold: [0, 1],
157+
});
158+
observer.observe(element);
159+
window.addEventListener("resize", handleResize);
160+
}
161+
});
162+
163+
onUnmounted(() => {
164+
if (observer) {
165+
observer.disconnect();
166+
}
167+
if (scrollingFn) {
168+
window.removeEventListener("scroll", scrollingFn);
169+
}
170+
window.removeEventListener("resize", handleResize);
171+
});
172+
</script>
173+
174+
<template>
175+
<!-- Hero -->
176+
<div
177+
class="h-[50vh] flex flex-col items-center justify-center text-center p-8"
178+
>
179+
<h1
180+
class="text-4xl md:text-6xl font-black mb-4 tracking-tighter text-slate-900"
181+
>
182+
Stacking Cards
183+
</h1>
184+
<p class="text-lg text-slate-500">Scroll down to see the effect.</p>
185+
</div>
186+
187+
<!-- Stacking Cards -->
188+
<ul ref="stackCardsRef" class="stack-cards">
189+
<!-- Card 1: Modern Design -->
190+
<li
191+
class="stack-cards__item bg-gradient-to-br from-indigo-500 to-purple-600 rounded-2xl shadow-2xl w-full max-w-5xl h-[60vh] mx-auto overflow-hidden"
192+
>
193+
<div
194+
class="absolute inset-0 flex flex-col justify-center items-center h-full p-8 text-white text-center"
195+
>
196+
<div class="mb-6 p-4 bg-white/20 rounded-full backdrop-blur-sm">
197+
<svg
198+
xmlns="http://www.w3.org/2000/svg"
199+
width="48"
200+
height="48"
201+
viewBox="0 0 24 24"
202+
fill="none"
203+
stroke="currentColor"
204+
stroke-width="2"
205+
stroke-linecap="round"
206+
stroke-linejoin="round"
207+
class="w-12 h-12 text-white"
208+
>
209+
<path d="m12 19 7-7 3 3-7 7-3-3z" />
210+
<path d="m18 13-1.5-7.5L2 2l3.5 14.5L13 18l5-5z" />
211+
<path d="m2 2 7.586 7.586" />
212+
<path d="M11 11a2 2 0 0 0 1.98 2.02" />
213+
<path d="m13.8 13a4 4 0 0 1 0 7.2" />
214+
</svg>
215+
</div>
216+
<h2 class="text-4xl md:text-5xl font-bold mb-4 tracking-tight">
217+
Modern Design
218+
</h2>
219+
<p class="text-xl md:text-2xl opacity-90 max-w-2xl font-light">
220+
Craft beautiful user interfaces with utility-first CSS and
221+
component-driven architecture.
222+
</p>
223+
</div>
224+
</li>
225+
226+
<!-- Card 2: High Performance -->
227+
<li
228+
class="stack-cards__item bg-slate-900 rounded-2xl shadow-2xl w-full max-w-5xl h-[60vh] mx-auto overflow-hidden border border-slate-800"
229+
>
230+
<div
231+
class="absolute inset-0 flex flex-col justify-center items-center h-full p-8 text-center relative group"
232+
>
233+
<div
234+
class="absolute inset-0 bg-gradient-to-tr from-emerald-500/10 to-transparent opacity-50"
235+
></div>
236+
<div
237+
class="relative z-10 mb-6 p-4 bg-emerald-500/10 rounded-full ring-1 ring-emerald-500/50"
238+
>
239+
<svg
240+
xmlns="http://www.w3.org/2000/svg"
241+
width="48"
242+
height="48"
243+
viewBox="0 0 24 24"
244+
fill="none"
245+
stroke="currentColor"
246+
stroke-width="2"
247+
stroke-linecap="round"
248+
stroke-linejoin="round"
249+
class="w-12 h-12 text-emerald-400"
250+
>
251+
<path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z" />
252+
</svg>
253+
</div>
254+
<h2
255+
class="relative z-10 text-4xl md:text-5xl font-bold mb-4 text-white tracking-tight"
256+
>
257+
High Performance
258+
</h2>
259+
<p
260+
class="relative z-10 text-xl md:text-2xl text-slate-400 max-w-2xl font-light"
261+
>
262+
Powered by Vue 3's reactive core for silky smooth animations and
263+
instant feedback.
264+
</p>
265+
</div>
266+
</li>
267+
268+
<!-- Card 3: Interactive -->
269+
<li
270+
class="stack-cards__item bg-white rounded-2xl shadow-2xl w-full max-w-5xl h-[60vh] mx-auto overflow-hidden border border-slate-100"
271+
>
272+
<div
273+
class="absolute inset-0 flex flex-col justify-center items-center h-full p-8 text-center"
274+
>
275+
<div class="mb-6 p-4 bg-blue-50 rounded-full">
276+
<svg
277+
xmlns="http://www.w3.org/2000/svg"
278+
width="48"
279+
height="48"
280+
viewBox="0 0 24 24"
281+
fill="none"
282+
stroke="currentColor"
283+
stroke-width="2"
284+
stroke-linecap="round"
285+
stroke-linejoin="round"
286+
class="w-12 h-12 text-blue-600"
287+
>
288+
<path
289+
d="M21 11V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h6"
290+
/>
291+
<path d="m12 12 4 10 1.7-4.3L22 16Z" />
292+
</svg>
293+
</div>
294+
<h2
295+
class="text-4xl md:text-5xl font-bold mb-4 text-slate-900 tracking-tight"
296+
>
297+
Interactive
298+
</h2>
299+
<p class="text-xl md:text-2xl text-slate-600 max-w-2xl mb-8 font-light">
300+
Engage users with scroll-driven interactions that bring your content
301+
to life.
302+
</p>
303+
<button
304+
class="px-8 py-3 bg-blue-600 hover:bg-blue-700 text-white font-semibold rounded-full transition-colors shadow-lg shadow-blue-600/30"
305+
>
306+
Get Started
307+
</button>
308+
</div>
309+
</li>
310+
</ul>
311+
312+
<!-- Footer -->
313+
<div class="h-screen flex items-center justify-center">
314+
<p class="text-slate-400">Footer content (scroll past cards)</p>
315+
</div>
316+
</template>
317+
318+
<style scoped>
319+
.stack-cards {
320+
--stack-cards-gap: 1.5rem;
321+
322+
@apply m-4;
323+
}
324+
325+
.stack-cards__item {
326+
@apply sticky top-15vh origin-top;
327+
}
328+
</style>

0 commit comments

Comments
 (0)