mirror of
https://github.com/DavidHDev/vue-bits.git
synced 2026-03-07 14:39:30 -07:00
Merge pull request #52 from Utkarsh-Singhal-26/feat/scroll-stack
Added <ScrollStack /> component
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
// Highlighted sidebar items
|
// Highlighted sidebar items
|
||||||
export const NEW = ['Target Cursor', 'Ripple Grid', 'Magic Bento', 'Galaxy', 'Text Type', 'Glass Surface', 'Sticker Peel'];
|
export const NEW = ['Target Cursor', 'Ripple Grid', 'Magic Bento', 'Galaxy', 'Text Type', 'Glass Surface', 'Sticker Peel', 'Scroll Stack'];
|
||||||
export const UPDATED = [];
|
export const UPDATED = [];
|
||||||
|
|
||||||
// Used for main sidebar navigation
|
// Used for main sidebar navigation
|
||||||
@@ -63,6 +63,7 @@ export const CATEGORIES = [
|
|||||||
'Masonry',
|
'Masonry',
|
||||||
'Glass Surface',
|
'Glass Surface',
|
||||||
'Magic Bento',
|
'Magic Bento',
|
||||||
|
'Scroll Stack',
|
||||||
'Profile Card',
|
'Profile Card',
|
||||||
'Dock',
|
'Dock',
|
||||||
'Gooey Nav',
|
'Gooey Nav',
|
||||||
@@ -80,7 +81,7 @@ export const CATEGORIES = [
|
|||||||
'Flowing Menu',
|
'Flowing Menu',
|
||||||
'Elastic Slider',
|
'Elastic Slider',
|
||||||
'Stack',
|
'Stack',
|
||||||
'Chroma Grid'
|
'Chroma Grid',
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ const components = {
|
|||||||
'tilted-card': () => import('../demo/Components/TiltedCardDemo.vue'),
|
'tilted-card': () => import('../demo/Components/TiltedCardDemo.vue'),
|
||||||
'stack': () => import('../demo/Components/StackDemo.vue'),
|
'stack': () => import('../demo/Components/StackDemo.vue'),
|
||||||
'chroma-grid': () => import('../demo/Components/ChromaGridDemo.vue'),
|
'chroma-grid': () => import('../demo/Components/ChromaGridDemo.vue'),
|
||||||
|
'scroll-stack': () => import('../demo/Components/ScrollStackDemo.vue'),
|
||||||
};
|
};
|
||||||
|
|
||||||
const backgrounds = {
|
const backgrounds = {
|
||||||
|
|||||||
26
src/constants/code/Components/scrollStackCode.ts
Normal file
26
src/constants/code/Components/scrollStackCode.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import code from '@content/Components/ScrollStack/ScrollStack.vue?raw';
|
||||||
|
import { createCodeObject } from '../../../types/code';
|
||||||
|
|
||||||
|
export const scrollStack = createCodeObject(code, 'Components/ScrollStack', {
|
||||||
|
installation: `npm install lenis`,
|
||||||
|
usage: `<template>
|
||||||
|
<ScrollStack>
|
||||||
|
<ScrollStackItem>
|
||||||
|
<h2>Card 1</h2>
|
||||||
|
<p>This is the first card in the stack</p>
|
||||||
|
</ScrollStackItem>
|
||||||
|
<ScrollStackItem>
|
||||||
|
<h2>Card 2</h2>
|
||||||
|
<p>This is the second card in the stack</p>
|
||||||
|
</ScrollStackItem>
|
||||||
|
<ScrollStackItem>
|
||||||
|
<h2>Card 3</h2>
|
||||||
|
<p>This is the third card in the stack</p>
|
||||||
|
</ScrollStackItem>
|
||||||
|
</ScrollStack>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import ScrollStack, { ScrollStackItem } from "./ScrollStack.vue";
|
||||||
|
</script>`
|
||||||
|
});
|
||||||
292
src/content/Components/ScrollStack/ScrollStack.vue
Normal file
292
src/content/Components/ScrollStack/ScrollStack.vue
Normal file
@@ -0,0 +1,292 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import Lenis from 'lenis';
|
||||||
|
import { defineComponent, h, nextTick, onBeforeUnmount, onMounted, ref, useTemplateRef, watch } from 'vue';
|
||||||
|
|
||||||
|
interface CardTransform {
|
||||||
|
translateY: number;
|
||||||
|
scale: number;
|
||||||
|
rotation: number;
|
||||||
|
blur: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ScrollStackProps {
|
||||||
|
className?: string;
|
||||||
|
itemDistance?: number;
|
||||||
|
itemScale?: number;
|
||||||
|
itemStackDistance?: number;
|
||||||
|
stackPosition?: string;
|
||||||
|
scaleEndPosition?: string;
|
||||||
|
baseScale?: number;
|
||||||
|
scaleDuration?: number;
|
||||||
|
rotationAmount?: number;
|
||||||
|
blurAmount?: number;
|
||||||
|
onStackComplete?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<ScrollStackProps>(), {
|
||||||
|
className: '',
|
||||||
|
itemDistance: 100,
|
||||||
|
itemScale: 0.03,
|
||||||
|
itemStackDistance: 30,
|
||||||
|
stackPosition: '20%',
|
||||||
|
scaleEndPosition: '10%',
|
||||||
|
baseScale: 0.85,
|
||||||
|
scaleDuration: 0.5,
|
||||||
|
rotationAmount: 0,
|
||||||
|
blurAmount: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
const scrollerRef = useTemplateRef('scrollerRef');
|
||||||
|
const stackCompletedRef = ref(false);
|
||||||
|
const animationFrameRef = ref<number | null>(null);
|
||||||
|
const lenisRef = ref<Lenis | null>(null);
|
||||||
|
const cardsRef = ref<HTMLElement[]>([]);
|
||||||
|
const lastTransformsRef = ref(new Map<number, CardTransform>());
|
||||||
|
const isUpdatingRef = ref(false);
|
||||||
|
|
||||||
|
const calculateProgress = (scrollTop: number, start: number, end: number) => {
|
||||||
|
if (scrollTop < start) return 0;
|
||||||
|
if (scrollTop > end) return 1;
|
||||||
|
return (scrollTop - start) / (end - start);
|
||||||
|
};
|
||||||
|
|
||||||
|
const parsePercentage = (value: string | number, containerHeight: number) => {
|
||||||
|
if (typeof value === 'string' && value.includes('%')) {
|
||||||
|
return (parseFloat(value) / 100) * containerHeight;
|
||||||
|
}
|
||||||
|
return parseFloat(value as string);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateCardTransforms = () => {
|
||||||
|
const scroller = scrollerRef.value;
|
||||||
|
if (!scroller || !cardsRef.value.length || isUpdatingRef.value) return;
|
||||||
|
|
||||||
|
isUpdatingRef.value = true;
|
||||||
|
|
||||||
|
const scrollTop = scroller.scrollTop;
|
||||||
|
const containerHeight = scroller.clientHeight;
|
||||||
|
const stackPositionPx = parsePercentage(props.stackPosition, containerHeight);
|
||||||
|
const scaleEndPositionPx = parsePercentage(props.scaleEndPosition, containerHeight);
|
||||||
|
const endElement = scroller.querySelector('.scroll-stack-end') as HTMLElement;
|
||||||
|
const endElementTop = endElement ? endElement.offsetTop : 0;
|
||||||
|
|
||||||
|
cardsRef.value.forEach((card, i) => {
|
||||||
|
if (!card) return;
|
||||||
|
|
||||||
|
const cardTop = card.offsetTop;
|
||||||
|
const triggerStart = cardTop - stackPositionPx - props.itemStackDistance * i;
|
||||||
|
const triggerEnd = cardTop - scaleEndPositionPx;
|
||||||
|
const pinStart = cardTop - stackPositionPx - props.itemStackDistance * i;
|
||||||
|
const pinEnd = endElementTop - containerHeight / 2;
|
||||||
|
|
||||||
|
const scaleProgress = calculateProgress(scrollTop, triggerStart, triggerEnd);
|
||||||
|
const targetScale = props.baseScale + i * props.itemScale;
|
||||||
|
const scale = 1 - scaleProgress * (1 - targetScale);
|
||||||
|
const rotation = props.rotationAmount ? i * props.rotationAmount * scaleProgress : 0;
|
||||||
|
|
||||||
|
let blur = 0;
|
||||||
|
if (props.blurAmount) {
|
||||||
|
let topCardIndex = 0;
|
||||||
|
for (let j = 0; j < cardsRef.value.length; j++) {
|
||||||
|
const jCardTop = cardsRef.value[j].offsetTop;
|
||||||
|
const jTriggerStart = jCardTop - stackPositionPx - props.itemStackDistance * j;
|
||||||
|
if (scrollTop >= jTriggerStart) {
|
||||||
|
topCardIndex = j;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i < topCardIndex) {
|
||||||
|
const depthInStack = topCardIndex - i;
|
||||||
|
blur = Math.max(0, depthInStack * props.blurAmount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let translateY = 0;
|
||||||
|
const isPinned = scrollTop >= pinStart && scrollTop <= pinEnd;
|
||||||
|
|
||||||
|
if (isPinned) {
|
||||||
|
translateY = scrollTop - cardTop + stackPositionPx + props.itemStackDistance * i;
|
||||||
|
} else if (scrollTop > pinEnd) {
|
||||||
|
translateY = pinEnd - cardTop + stackPositionPx + props.itemStackDistance * i;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newTransform = {
|
||||||
|
translateY: Math.round(translateY * 100) / 100,
|
||||||
|
scale: Math.round(scale * 1000) / 1000,
|
||||||
|
rotation: Math.round(rotation * 100) / 100,
|
||||||
|
blur: Math.round(blur * 100) / 100
|
||||||
|
};
|
||||||
|
|
||||||
|
const lastTransform = lastTransformsRef.value.get(i);
|
||||||
|
const hasChanged =
|
||||||
|
!lastTransform ||
|
||||||
|
Math.abs(lastTransform.translateY - newTransform.translateY) > 0.1 ||
|
||||||
|
Math.abs(lastTransform.scale - newTransform.scale) > 0.001 ||
|
||||||
|
Math.abs(lastTransform.rotation - newTransform.rotation) > 0.1 ||
|
||||||
|
Math.abs(lastTransform.blur - newTransform.blur) > 0.1;
|
||||||
|
|
||||||
|
if (hasChanged) {
|
||||||
|
const transform = `translate3d(0, ${newTransform.translateY}px, 0) scale(${newTransform.scale}) rotate(${newTransform.rotation}deg)`;
|
||||||
|
const filter = newTransform.blur > 0 ? `blur(${newTransform.blur}px)` : '';
|
||||||
|
|
||||||
|
card.style.transform = transform;
|
||||||
|
card.style.filter = filter;
|
||||||
|
|
||||||
|
lastTransformsRef.value.set(i, newTransform);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i === cardsRef.value.length - 1) {
|
||||||
|
const isInView = scrollTop >= pinStart && scrollTop <= pinEnd;
|
||||||
|
if (isInView && !stackCompletedRef.value) {
|
||||||
|
stackCompletedRef.value = true;
|
||||||
|
props.onStackComplete?.();
|
||||||
|
} else if (!isInView && stackCompletedRef.value) {
|
||||||
|
stackCompletedRef.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
isUpdatingRef.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleScroll = () => {
|
||||||
|
updateCardTransforms();
|
||||||
|
};
|
||||||
|
|
||||||
|
const setupLenis = () => {
|
||||||
|
const scroller = scrollerRef.value;
|
||||||
|
if (!scroller) return;
|
||||||
|
|
||||||
|
const lenis = new Lenis({
|
||||||
|
wrapper: scroller,
|
||||||
|
content: scroller.querySelector('.scroll-stack-inner') as HTMLElement,
|
||||||
|
duration: 1.2,
|
||||||
|
easing: t => Math.min(1, 1.001 - Math.pow(2, -10 * t)),
|
||||||
|
smoothWheel: true,
|
||||||
|
touchMultiplier: 2,
|
||||||
|
infinite: false,
|
||||||
|
gestureOrientation: 'vertical',
|
||||||
|
wheelMultiplier: 1,
|
||||||
|
lerp: 0.1,
|
||||||
|
syncTouch: true,
|
||||||
|
syncTouchLerp: 0.075
|
||||||
|
});
|
||||||
|
|
||||||
|
lenis.on('scroll', handleScroll);
|
||||||
|
|
||||||
|
const raf = (time: number) => {
|
||||||
|
lenis.raf(time);
|
||||||
|
animationFrameRef.value = requestAnimationFrame(raf);
|
||||||
|
};
|
||||||
|
animationFrameRef.value = requestAnimationFrame(raf);
|
||||||
|
|
||||||
|
lenisRef.value = lenis;
|
||||||
|
return lenis;
|
||||||
|
};
|
||||||
|
|
||||||
|
let cleanup: (() => void) | null = null;
|
||||||
|
const setup = () => {
|
||||||
|
const scroller = scrollerRef.value;
|
||||||
|
if (!scroller) return;
|
||||||
|
|
||||||
|
const cards = Array.from(scroller.querySelectorAll('.scroll-stack-card')) as HTMLElement[];
|
||||||
|
cardsRef.value = cards;
|
||||||
|
const transformsCache = lastTransformsRef.value;
|
||||||
|
|
||||||
|
cards.forEach((card, i) => {
|
||||||
|
if (i < cards.length - 1) {
|
||||||
|
card.style.marginBottom = `${props.itemDistance}px`;
|
||||||
|
}
|
||||||
|
card.style.willChange = 'transform, filter';
|
||||||
|
card.style.transformOrigin = 'top center';
|
||||||
|
card.style.backfaceVisibility = 'hidden';
|
||||||
|
card.style.transform = 'translateZ(0)';
|
||||||
|
card.style.webkitTransform = 'translateZ(0)';
|
||||||
|
card.style.perspective = '1000px';
|
||||||
|
card.style.webkitPerspective = '1000px';
|
||||||
|
});
|
||||||
|
|
||||||
|
setupLenis();
|
||||||
|
|
||||||
|
updateCardTransforms();
|
||||||
|
|
||||||
|
cleanup = () => {
|
||||||
|
if (animationFrameRef.value) {
|
||||||
|
cancelAnimationFrame(animationFrameRef.value);
|
||||||
|
}
|
||||||
|
if (lenisRef.value) {
|
||||||
|
lenisRef.value.destroy();
|
||||||
|
}
|
||||||
|
stackCompletedRef.value = false;
|
||||||
|
cardsRef.value = [];
|
||||||
|
transformsCache.clear();
|
||||||
|
isUpdatingRef.value = false;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await nextTick();
|
||||||
|
setup();
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
cleanup?.();
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props,
|
||||||
|
() => {
|
||||||
|
cleanup?.();
|
||||||
|
setup();
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export const ScrollStackItem = defineComponent({
|
||||||
|
name: 'ScrollStackItem',
|
||||||
|
props: {
|
||||||
|
itemClassName: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setup(props, { slots }) {
|
||||||
|
return () =>
|
||||||
|
h(
|
||||||
|
'div',
|
||||||
|
{
|
||||||
|
class:
|
||||||
|
`scroll-stack-card relative w-full h-80 my-8 p-12 rounded-[40px] shadow-[0_0_30px_rgba(0,0,0,0.1)] box-border origin-top will-change-transform ${props.itemClassName}`.trim(),
|
||||||
|
style: {
|
||||||
|
backfaceVisibility: 'hidden',
|
||||||
|
transformStyle: 'preserve-3d'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
slots.default?.()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
ref="scrollerRef"
|
||||||
|
:class="['relative w-full h-full overflow-y-auto overflow-x-visible', className]"
|
||||||
|
:style="{
|
||||||
|
overscrollBehavior: 'contain',
|
||||||
|
WebkitOverflowScrolling: 'touch',
|
||||||
|
scrollBehavior: 'smooth',
|
||||||
|
WebkitTransform: 'translateZ(0)',
|
||||||
|
transform: 'translateZ(0)',
|
||||||
|
willChange: 'scroll-position'
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div class="px-20 pt-[20vh] pb-[50rem] min-h-screen scroll-stack-inner">
|
||||||
|
<slot />
|
||||||
|
<!-- Spacer so the last pin can release cleanly -->
|
||||||
|
<div class="w-full h-px scroll-stack-end" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -566,3 +566,90 @@ div:has(> .props-table) {
|
|||||||
order: 2;
|
order: 2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.scroll-stack-card-demo {
|
||||||
|
font-size: clamp(1.5rem, 4vw, 3rem);
|
||||||
|
font-weight: 900;
|
||||||
|
color: #fff;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroll-stack-card-demo .stack-img-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 150px;
|
||||||
|
border-radius: 1.5rem;
|
||||||
|
border: 10px solid #fff;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: clamp(4rem, 8vw, 8rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroll-stack-demo-container .scroll-stack-inner {
|
||||||
|
padding: 20vh 2rem 50rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ssc-demo-1 {
|
||||||
|
background-color: #5227ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ssc-demo-2 {
|
||||||
|
background-color: #f01e9c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ssc-demo-3 {
|
||||||
|
background-color: #5227ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ssc-demo-4 {
|
||||||
|
background-color: #f01e9c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ssc-demo-5 {
|
||||||
|
background-color: #5227ff;
|
||||||
|
text-align: center;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (min-width: 1240px) {
|
||||||
|
.scroll-stack-card-demo {
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 2rem;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroll-stack-demo-container .scroll-stack-inner {
|
||||||
|
padding: 20vh 5rem 50rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroll-stack-card-demo .stack-img-container {
|
||||||
|
width: 50%;
|
||||||
|
min-height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroll-stack-card-demo h3 {
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 480px) {
|
||||||
|
.scroll-stack-card-demo {
|
||||||
|
font-size: 1rem;
|
||||||
|
padding: 0.2rem;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroll-stack-card-demo .stack-img-container {
|
||||||
|
border-width: 5px;
|
||||||
|
border-radius: 1rem;
|
||||||
|
min-height: 120px;
|
||||||
|
font-size: 3rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
216
src/demo/Components/ScrollStackDemo.vue
Normal file
216
src/demo/Components/ScrollStackDemo.vue
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
<template>
|
||||||
|
<TabbedLayout>
|
||||||
|
<template #preview>
|
||||||
|
<div class="relative h-[500px] overflow-hidden demo-container">
|
||||||
|
<RefreshButton
|
||||||
|
@refresh="
|
||||||
|
() => {
|
||||||
|
isCompleted = false;
|
||||||
|
forceRerender();
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<p
|
||||||
|
class="top-[25%] left-[50%] absolute font-black text-[#271e37] text-[clamp(2rem,4vw,3rem)] text-center transition-all -translate-x-1/2 -translate-y-1/2 duration-300 ease-in-out pointer-events-none transform"
|
||||||
|
>
|
||||||
|
{{ isCompleted ? 'Stack Completed!' : 'Scroll Down' }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ScrollStack
|
||||||
|
:key="rerenderKey"
|
||||||
|
:item-distance="itemDistance"
|
||||||
|
className="scroll-stack-demo-container"
|
||||||
|
:item-stack-distance="itemStackDistance"
|
||||||
|
:stack-position="stackPosition"
|
||||||
|
:base-scale="baseScale"
|
||||||
|
:rotation-amount="rotationAmount"
|
||||||
|
:blur-amount="blurAmount"
|
||||||
|
@stackComplete="handleStackComplete"
|
||||||
|
>
|
||||||
|
<ScrollStackItem itemClassName="scroll-stack-card-demo ssc-demo-1">
|
||||||
|
<h3>Text Animations</h3>
|
||||||
|
|
||||||
|
<div className="stack-img-container">
|
||||||
|
<i class="pi-align-left pi" style="font-size: 120px"></i>
|
||||||
|
</div>
|
||||||
|
</ScrollStackItem>
|
||||||
|
|
||||||
|
<ScrollStackItem itemClassName="scroll-stack-card-demo ssc-demo-2">
|
||||||
|
<h3>Animations</h3>
|
||||||
|
|
||||||
|
<div className="stack-img-container">
|
||||||
|
<i class="pi pi-play" style="font-size: 120px"></i>
|
||||||
|
</div>
|
||||||
|
</ScrollStackItem>
|
||||||
|
|
||||||
|
<ScrollStackItem itemClassName="scroll-stack-card-demo ssc-demo-3">
|
||||||
|
<h3>Components</h3>
|
||||||
|
|
||||||
|
<div className="stack-img-container">
|
||||||
|
<i class="pi pi-sliders-h" style="font-size: 120px"></i>
|
||||||
|
</div>
|
||||||
|
</ScrollStackItem>
|
||||||
|
|
||||||
|
<ScrollStackItem itemClassName="scroll-stack-card-demo ssc-demo-4">
|
||||||
|
<h3>Backgrounds</h3>
|
||||||
|
|
||||||
|
<div className="stack-img-container">
|
||||||
|
<i class="pi pi-image" style="font-size: 120px"></i>
|
||||||
|
</div>
|
||||||
|
</ScrollStackItem>
|
||||||
|
|
||||||
|
<ScrollStackItem itemClassName="scroll-stack-card-demo ssc-demo-5">
|
||||||
|
<h3>All on Vue Bits!</h3>
|
||||||
|
</ScrollStackItem>
|
||||||
|
</ScrollStack>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Customize>
|
||||||
|
<PreviewSlider title="Item Distance" v-model="itemDistance" :min="0" :max="1000" :step="10" value-unit="px" />
|
||||||
|
<PreviewSlider
|
||||||
|
title="Stack Distance"
|
||||||
|
v-model="itemStackDistance"
|
||||||
|
:min="0"
|
||||||
|
:max="40"
|
||||||
|
:step="5"
|
||||||
|
value-unit="px"
|
||||||
|
/>
|
||||||
|
<PreviewSelect title="Stack Position" v-model="stackPosition" :options="stackPositionOptions" />
|
||||||
|
<PreviewSlider title="Base Scale" v-model="baseScale" :min="0.5" :max="1.0" :step="0.05" />
|
||||||
|
<PreviewSlider title="Rotation Amount" v-model="rotationAmount" :min="0" :max="1" :step="0.1" value-unit="°" />
|
||||||
|
<PreviewSlider title="Blur Amount" v-model="blurAmount" :min="0" :max="10" :step="0.5" value-unit="px" />
|
||||||
|
</Customize>
|
||||||
|
|
||||||
|
<PropTable :data="propData" />
|
||||||
|
<Dependencies :dependency-list="['lenis']" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #code>
|
||||||
|
<CodeExample :code-object="scrollStack" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #cli>
|
||||||
|
<CliInstallation :command="scrollStack.cli" />
|
||||||
|
</template>
|
||||||
|
</TabbedLayout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import PreviewSelect from '@/components/common/PreviewSelect.vue';
|
||||||
|
import { useForceRerender } from '@/composables/useForceRerender';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import CliInstallation from '../../components/code/CliInstallation.vue';
|
||||||
|
import CodeExample from '../../components/code/CodeExample.vue';
|
||||||
|
import Dependencies from '../../components/code/Dependencies.vue';
|
||||||
|
import Customize from '../../components/common/Customize.vue';
|
||||||
|
import PreviewSlider from '../../components/common/PreviewSlider.vue';
|
||||||
|
import PropTable from '../../components/common/PropTable.vue';
|
||||||
|
import RefreshButton from '../../components/common/RefreshButton.vue';
|
||||||
|
import TabbedLayout from '../../components/common/TabbedLayout.vue';
|
||||||
|
import { scrollStack } from '../../constants/code/Components/scrollStackCode';
|
||||||
|
import ScrollStack, { ScrollStackItem } from '../../content/Components/ScrollStack/ScrollStack.vue';
|
||||||
|
|
||||||
|
const { rerenderKey, forceRerender } = useForceRerender();
|
||||||
|
|
||||||
|
const isCompleted = ref(false);
|
||||||
|
const itemDistance = ref(200);
|
||||||
|
const itemStackDistance = ref(30);
|
||||||
|
const baseScale = ref(0.85);
|
||||||
|
const rotationAmount = ref(0);
|
||||||
|
const blurAmount = ref(0);
|
||||||
|
const stackPosition = ref('20%');
|
||||||
|
|
||||||
|
const handleStackComplete = () => {
|
||||||
|
isCompleted.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const stackPositionOptions = [
|
||||||
|
{ value: '10%', label: '10%' },
|
||||||
|
{ value: '15%', label: '15%' },
|
||||||
|
{ value: '20%', label: '20%' },
|
||||||
|
{ value: '25%', label: '25%' },
|
||||||
|
{ value: '30%', label: '30%' },
|
||||||
|
{ value: '35%', label: '35%' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const propData = [
|
||||||
|
{
|
||||||
|
name: 'children',
|
||||||
|
type: 'ReactNode',
|
||||||
|
default: 'required',
|
||||||
|
description: 'The content to be displayed in the scroll stack. Should contain ScrollStackItem components.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'className',
|
||||||
|
type: 'string',
|
||||||
|
default: '""',
|
||||||
|
description: 'Additional CSS classes to apply to the scroll stack container.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'itemDistance',
|
||||||
|
type: 'number',
|
||||||
|
default: '100',
|
||||||
|
description: 'Distance between stacked items in pixels.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'itemScale',
|
||||||
|
type: 'number',
|
||||||
|
default: '0.03',
|
||||||
|
description: 'Scale increment for each stacked item.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'itemStackDistance',
|
||||||
|
type: 'number',
|
||||||
|
default: '30',
|
||||||
|
description: 'Distance between items when they start stacking.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'stackPosition',
|
||||||
|
type: 'string',
|
||||||
|
default: '"20%"',
|
||||||
|
description: 'Position where the stacking effect begins as a percentage of viewport height.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'scaleEndPosition',
|
||||||
|
type: 'string',
|
||||||
|
default: '"10%"',
|
||||||
|
description: 'Position where the scaling effect ends as a percentage of viewport height.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'baseScale',
|
||||||
|
type: 'number',
|
||||||
|
default: '0.85',
|
||||||
|
description: 'Base scale value for the first item in the stack.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'scaleDuration',
|
||||||
|
type: 'number',
|
||||||
|
default: '0.5',
|
||||||
|
description: 'Duration of the scaling animation in seconds.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'rotationAmount',
|
||||||
|
type: 'number',
|
||||||
|
default: '0',
|
||||||
|
description: 'Rotation amount for each item in degrees.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'blurAmount',
|
||||||
|
type: 'number',
|
||||||
|
default: '0',
|
||||||
|
description: 'Blur amount for items that are further back in the stack.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'onStackComplete',
|
||||||
|
type: 'function',
|
||||||
|
default: 'undefined',
|
||||||
|
description: 'Callback function called when the stack animation is complete.'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.demo-container {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user