mirror of
https://github.com/DavidHDev/vue-bits.git
synced 2026-03-07 14:39:30 -07:00
Added <GradualBlur /> Animation
This commit is contained in:
@@ -24,6 +24,7 @@
|
|||||||
"@wdns/vue-code-block": "^2.3.5",
|
"@wdns/vue-code-block": "^2.3.5",
|
||||||
"gsap": "^3.13.0",
|
"gsap": "^3.13.0",
|
||||||
"lenis": "^1.3.8",
|
"lenis": "^1.3.8",
|
||||||
|
"mathjs": "^14.6.0",
|
||||||
"matter-js": "^0.20.0",
|
"matter-js": "^0.20.0",
|
||||||
"motion-v": "^1.5.0",
|
"motion-v": "^1.5.0",
|
||||||
"ogl": "^1.0.11",
|
"ogl": "^1.0.11",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Highlighted sidebar items
|
// Highlighted sidebar items
|
||||||
export const NEW = ['Gradient Blinds', 'Bubble Menu', 'Prism', 'Plasma', 'Electric Border', 'Target Cursor', 'Pill Nav', 'Card Nav', 'Logo Loop', 'Prismatic Burst'];
|
export const NEW = ['Gradual Blur', 'Gradient Blinds', 'Bubble Menu', 'Prism', 'Plasma', 'Electric Border', 'Target Cursor', 'Pill Nav', 'Card Nav', 'Logo Loop', 'Prismatic Burst'];
|
||||||
export const UPDATED = [];
|
export const UPDATED = [];
|
||||||
|
|
||||||
// Used for main sidebar navigation
|
// Used for main sidebar navigation
|
||||||
@@ -36,6 +36,7 @@ export const CATEGORIES = [
|
|||||||
subcategories: [
|
subcategories: [
|
||||||
'Animated Content',
|
'Animated Content',
|
||||||
'Fade Content',
|
'Fade Content',
|
||||||
|
'Gradual Blur',
|
||||||
'Noise',
|
'Noise',
|
||||||
'Splash Cursor',
|
'Splash Cursor',
|
||||||
'Logo Loop',
|
'Logo Loop',
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ const animations = {
|
|||||||
'crosshair': () => import('../demo/Animations/CrosshairDemo.vue'),
|
'crosshair': () => import('../demo/Animations/CrosshairDemo.vue'),
|
||||||
'sticker-peel': () => import('../demo/Animations/StickerPeelDemo.vue'),
|
'sticker-peel': () => import('../demo/Animations/StickerPeelDemo.vue'),
|
||||||
'electric-border': () => import('../demo/Animations/ElectricBorderDemo.vue'),
|
'electric-border': () => import('../demo/Animations/ElectricBorderDemo.vue'),
|
||||||
|
'gradual-blur': () => import('../demo/Animations/GradualBlurDemo.vue'),
|
||||||
};
|
};
|
||||||
|
|
||||||
const textAnimations = {
|
const textAnimations = {
|
||||||
|
|||||||
11
src/constants/code/Animations/gradualBlurCode.ts
Normal file
11
src/constants/code/Animations/gradualBlurCode.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import code from '@/content/Animations/GradualBlur/GradualBlur.vue?raw';
|
||||||
|
import { createCodeObject } from '@/types/code';
|
||||||
|
|
||||||
|
export const gradualBlur = createCodeObject(code, 'Animations/GradualBlur', {
|
||||||
|
installation: `npm install mathjs`,
|
||||||
|
usage: `
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import GradualBlur from "./GradualBlur.vue";
|
||||||
|
</script>`
|
||||||
|
});
|
||||||
350
src/content/Animations/GradualBlur/GradualBlur.vue
Normal file
350
src/content/Animations/GradualBlur/GradualBlur.vue
Normal file
@@ -0,0 +1,350 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import * as math from 'mathjs';
|
||||||
|
import { computed, onMounted, onUnmounted, ref, watch, type CSSProperties, type StyleValue } from 'vue';
|
||||||
|
|
||||||
|
export type GradualBlurProps = {
|
||||||
|
position?: 'top' | 'bottom' | 'left' | 'right';
|
||||||
|
strength?: number;
|
||||||
|
height?: string;
|
||||||
|
width?: string;
|
||||||
|
divCount?: number;
|
||||||
|
exponential?: boolean;
|
||||||
|
zIndex?: number;
|
||||||
|
animated?: boolean | 'scroll';
|
||||||
|
duration?: string;
|
||||||
|
easing?: string;
|
||||||
|
opacity?: number;
|
||||||
|
curve?: 'linear' | 'bezier' | 'ease-in' | 'ease-out' | 'ease-in-out';
|
||||||
|
responsive?: boolean;
|
||||||
|
mobileHeight?: string;
|
||||||
|
tabletHeight?: string;
|
||||||
|
desktopHeight?: string;
|
||||||
|
mobileWidth?: string;
|
||||||
|
tabletWidth?: string;
|
||||||
|
desktopWidth?: string;
|
||||||
|
preset?:
|
||||||
|
| 'top'
|
||||||
|
| 'bottom'
|
||||||
|
| 'left'
|
||||||
|
| 'right'
|
||||||
|
| 'subtle'
|
||||||
|
| 'intense'
|
||||||
|
| 'smooth'
|
||||||
|
| 'sharp'
|
||||||
|
| 'header'
|
||||||
|
| 'footer'
|
||||||
|
| 'sidebar'
|
||||||
|
| 'page-header'
|
||||||
|
| 'page-footer';
|
||||||
|
gpuOptimized?: boolean;
|
||||||
|
hoverIntensity?: number;
|
||||||
|
target?: 'parent' | 'page';
|
||||||
|
onAnimationComplete?: () => void;
|
||||||
|
className?: string;
|
||||||
|
style?: CSSProperties;
|
||||||
|
};
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<GradualBlurProps>(), {
|
||||||
|
position: 'bottom',
|
||||||
|
strength: 2,
|
||||||
|
height: '6rem',
|
||||||
|
divCount: 5,
|
||||||
|
exponential: false,
|
||||||
|
zIndex: 1000,
|
||||||
|
animated: false,
|
||||||
|
duration: '0.3s',
|
||||||
|
easing: 'ease-out',
|
||||||
|
opacity: 1,
|
||||||
|
curve: 'linear',
|
||||||
|
responsive: false,
|
||||||
|
target: 'parent',
|
||||||
|
className: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
const DEFAULT_CONFIG: Partial<GradualBlurProps> = {
|
||||||
|
position: 'bottom',
|
||||||
|
strength: 2,
|
||||||
|
height: '6rem',
|
||||||
|
divCount: 5,
|
||||||
|
exponential: false,
|
||||||
|
zIndex: 1000,
|
||||||
|
animated: false,
|
||||||
|
duration: '0.3s',
|
||||||
|
easing: 'ease-out',
|
||||||
|
opacity: 1,
|
||||||
|
curve: 'linear',
|
||||||
|
responsive: false,
|
||||||
|
target: 'parent',
|
||||||
|
className: '',
|
||||||
|
style: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
const PRESETS: Record<string, Partial<GradualBlurProps>> = {
|
||||||
|
top: { position: 'top', height: '6rem' },
|
||||||
|
bottom: { position: 'bottom', height: '6rem' },
|
||||||
|
left: { position: 'left', height: '6rem' },
|
||||||
|
right: { position: 'right', height: '6rem' },
|
||||||
|
|
||||||
|
subtle: { height: '4rem', strength: 1, opacity: 0.8, divCount: 3 },
|
||||||
|
intense: { height: '10rem', strength: 4, divCount: 8, exponential: true },
|
||||||
|
|
||||||
|
smooth: { height: '8rem', curve: 'bezier', divCount: 10 },
|
||||||
|
sharp: { height: '5rem', curve: 'linear', divCount: 4 },
|
||||||
|
|
||||||
|
header: { position: 'top', height: '8rem', curve: 'ease-out' },
|
||||||
|
footer: { position: 'bottom', height: '8rem', curve: 'ease-out' },
|
||||||
|
sidebar: { position: 'left', height: '6rem', strength: 2.5 },
|
||||||
|
|
||||||
|
'page-header': {
|
||||||
|
position: 'top',
|
||||||
|
height: '10rem',
|
||||||
|
target: 'page',
|
||||||
|
strength: 3
|
||||||
|
},
|
||||||
|
'page-footer': {
|
||||||
|
position: 'bottom',
|
||||||
|
height: '10rem',
|
||||||
|
target: 'page',
|
||||||
|
strength: 3
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const CURVE_FUNCTIONS: Record<string, (p: number) => number> = {
|
||||||
|
linear: p => p,
|
||||||
|
bezier: p => p * p * (3 - 2 * p),
|
||||||
|
'ease-in': p => p * p,
|
||||||
|
'ease-out': p => 1 - Math.pow(1 - p, 2),
|
||||||
|
'ease-in-out': p => (p < 0.5 ? 2 * p * p : 1 - Math.pow(-2 * p + 2, 2) / 2)
|
||||||
|
};
|
||||||
|
|
||||||
|
const containerRef = ref<HTMLDivElement | null>(null);
|
||||||
|
const isHovered = ref(false);
|
||||||
|
const isVisible = ref(true);
|
||||||
|
const responsiveHeight = ref(props.height);
|
||||||
|
const responsiveWidth = ref(props.width);
|
||||||
|
|
||||||
|
const config = computed(() => {
|
||||||
|
const presetConfig = props.preset && PRESETS[props.preset] ? PRESETS[props.preset] : {};
|
||||||
|
return {
|
||||||
|
...DEFAULT_CONFIG,
|
||||||
|
...presetConfig,
|
||||||
|
...props
|
||||||
|
} as Required<GradualBlurProps>;
|
||||||
|
});
|
||||||
|
|
||||||
|
const getGradientDirection = (position: string): string => {
|
||||||
|
const directions: Record<string, string> = {
|
||||||
|
top: 'to top',
|
||||||
|
bottom: 'to bottom',
|
||||||
|
left: 'to left',
|
||||||
|
right: 'to right'
|
||||||
|
};
|
||||||
|
return directions[position] || 'to bottom';
|
||||||
|
};
|
||||||
|
|
||||||
|
const debounce = <T extends (...a: unknown[]) => void>(fn: T, wait: number) => {
|
||||||
|
let timeout: ReturnType<typeof setTimeout>;
|
||||||
|
return (...args: Parameters<T>) => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
timeout = setTimeout(() => fn(...args), wait);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateResponsiveDimensions = () => {
|
||||||
|
if (!config.value.responsive) return;
|
||||||
|
|
||||||
|
const width = window.innerWidth;
|
||||||
|
const currentConfig = config.value;
|
||||||
|
|
||||||
|
let newHeight = currentConfig.height;
|
||||||
|
if (width <= 480 && currentConfig.mobileHeight) {
|
||||||
|
newHeight = currentConfig.mobileHeight;
|
||||||
|
} else if (width <= 768 && currentConfig.tabletHeight) {
|
||||||
|
newHeight = currentConfig.tabletHeight;
|
||||||
|
} else if (width <= 1024 && currentConfig.desktopHeight) {
|
||||||
|
newHeight = currentConfig.desktopHeight;
|
||||||
|
}
|
||||||
|
responsiveHeight.value = newHeight;
|
||||||
|
|
||||||
|
let newWidth = currentConfig.width;
|
||||||
|
if (width <= 480 && currentConfig.mobileWidth) {
|
||||||
|
newWidth = currentConfig.mobileWidth;
|
||||||
|
} else if (width <= 768 && currentConfig.tabletWidth) {
|
||||||
|
newWidth = currentConfig.tabletWidth;
|
||||||
|
} else if (width <= 1024 && currentConfig.desktopWidth) {
|
||||||
|
newWidth = currentConfig.desktopWidth;
|
||||||
|
}
|
||||||
|
responsiveWidth.value = newWidth;
|
||||||
|
};
|
||||||
|
|
||||||
|
let intersectionObserver: IntersectionObserver | null = null;
|
||||||
|
|
||||||
|
const setupIntersectionObserver = () => {
|
||||||
|
if (config.value.animated !== 'scroll' || !containerRef.value) return;
|
||||||
|
|
||||||
|
intersectionObserver = new IntersectionObserver(
|
||||||
|
([entry]) => {
|
||||||
|
isVisible.value = entry.isIntersecting;
|
||||||
|
},
|
||||||
|
{ threshold: 0.1 }
|
||||||
|
);
|
||||||
|
|
||||||
|
intersectionObserver.observe(containerRef.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const blurDivs = computed(() => {
|
||||||
|
const divs: Array<{ style: CSSProperties }> = [];
|
||||||
|
const increment = 100 / config.value.divCount;
|
||||||
|
const currentStrength =
|
||||||
|
isHovered.value && config.value.hoverIntensity
|
||||||
|
? config.value.strength * config.value.hoverIntensity
|
||||||
|
: config.value.strength;
|
||||||
|
|
||||||
|
const curveFunc = CURVE_FUNCTIONS[config.value.curve] || CURVE_FUNCTIONS.linear;
|
||||||
|
|
||||||
|
for (let i = 1; i <= config.value.divCount; i++) {
|
||||||
|
let progress = i / config.value.divCount;
|
||||||
|
progress = curveFunc(progress);
|
||||||
|
|
||||||
|
let blurValue: number;
|
||||||
|
if (config.value.exponential) {
|
||||||
|
blurValue = Number(math.pow(2, progress * 4)) * 0.0625 * currentStrength;
|
||||||
|
} else {
|
||||||
|
blurValue = 0.0625 * (progress * config.value.divCount + 1) * currentStrength;
|
||||||
|
}
|
||||||
|
|
||||||
|
const p1 = math.round((increment * i - increment) * 10) / 10;
|
||||||
|
const p2 = math.round(increment * i * 10) / 10;
|
||||||
|
const p3 = math.round((increment * i + increment) * 10) / 10;
|
||||||
|
const p4 = math.round((increment * i + increment * 2) * 10) / 10;
|
||||||
|
|
||||||
|
let gradient = `transparent ${p1}%, black ${p2}%`;
|
||||||
|
if (p3 <= 100) gradient += `, black ${p3}%`;
|
||||||
|
if (p4 <= 100) gradient += `, transparent ${p4}%`;
|
||||||
|
|
||||||
|
const direction = getGradientDirection(config.value.position);
|
||||||
|
|
||||||
|
const divStyle: CSSProperties = {
|
||||||
|
maskImage: `linear-gradient(${direction}, ${gradient})`,
|
||||||
|
WebkitMaskImage: `linear-gradient(${direction}, ${gradient})`,
|
||||||
|
backdropFilter: `blur(${blurValue.toFixed(3)}rem)`,
|
||||||
|
opacity: config.value.opacity,
|
||||||
|
transition:
|
||||||
|
config.value.animated && config.value.animated !== 'scroll'
|
||||||
|
? `backdrop-filter ${config.value.duration} ${config.value.easing}`
|
||||||
|
: undefined
|
||||||
|
};
|
||||||
|
|
||||||
|
divs.push({ style: divStyle });
|
||||||
|
}
|
||||||
|
|
||||||
|
return divs;
|
||||||
|
});
|
||||||
|
|
||||||
|
const containerStyle = computed((): StyleValue => {
|
||||||
|
const isVertical = ['top', 'bottom'].includes(config.value.position);
|
||||||
|
const isHorizontal = ['left', 'right'].includes(config.value.position);
|
||||||
|
const isPageTarget = config.value.target === 'page';
|
||||||
|
|
||||||
|
const baseStyle: CSSProperties = {
|
||||||
|
position: isPageTarget ? 'fixed' : 'absolute',
|
||||||
|
pointerEvents: config.value.hoverIntensity ? 'auto' : 'none',
|
||||||
|
opacity: isVisible.value ? 1 : 0,
|
||||||
|
transition: config.value.animated ? `opacity ${config.value.duration} ${config.value.easing}` : undefined,
|
||||||
|
zIndex: isPageTarget ? config.value.zIndex + 100 : config.value.zIndex,
|
||||||
|
...config.value.style
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isVertical) {
|
||||||
|
baseStyle.height = responsiveHeight.value;
|
||||||
|
baseStyle.width = responsiveWidth.value || '100%';
|
||||||
|
baseStyle[config.value.position] = '0';
|
||||||
|
baseStyle.left = '0';
|
||||||
|
baseStyle.right = '0';
|
||||||
|
} else if (isHorizontal) {
|
||||||
|
baseStyle.width = responsiveWidth.value || responsiveHeight.value;
|
||||||
|
baseStyle.height = '100%';
|
||||||
|
baseStyle[config.value.position] = '0';
|
||||||
|
baseStyle.top = '0';
|
||||||
|
baseStyle.bottom = '0';
|
||||||
|
}
|
||||||
|
|
||||||
|
return baseStyle;
|
||||||
|
});
|
||||||
|
|
||||||
|
const debouncedResize = debounce(updateResponsiveDimensions, 100);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// Initialize responsive dimensions
|
||||||
|
if (config.value.responsive) {
|
||||||
|
updateResponsiveDimensions();
|
||||||
|
window.addEventListener('resize', debouncedResize);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.value.animated === 'scroll') {
|
||||||
|
isVisible.value = false;
|
||||||
|
setupIntersectionObserver();
|
||||||
|
}
|
||||||
|
|
||||||
|
injectStyles();
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (config.value.responsive) {
|
||||||
|
window.removeEventListener('resize', debouncedResize);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (intersectionObserver) {
|
||||||
|
intersectionObserver.disconnect();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => isVisible.value,
|
||||||
|
newVisible => {
|
||||||
|
if (newVisible && config.value.animated === 'scroll' && props.onAnimationComplete) {
|
||||||
|
const timeout = setTimeout(
|
||||||
|
() => {
|
||||||
|
if (props.onAnimationComplete) {
|
||||||
|
props.onAnimationComplete();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
parseFloat(config.value.duration) * 1000
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => clearTimeout(timeout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const injectStyles = () => {
|
||||||
|
if (typeof document === 'undefined') return;
|
||||||
|
const id = 'gradual-blur-styles';
|
||||||
|
if (document.getElementById(id)) return;
|
||||||
|
const el = document.createElement('style');
|
||||||
|
el.id = id;
|
||||||
|
el.textContent = `.gradual-blur{pointer-events:none;transition:opacity .3s ease-out}.gradual-blur-inner{pointer-events:none}`;
|
||||||
|
document.head.appendChild(el);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
ref="containerRef"
|
||||||
|
:class="[
|
||||||
|
'gradual-blur relative isolate',
|
||||||
|
config.target === 'page' ? 'gradual-blur-page' : 'gradual-blur-parent',
|
||||||
|
config.className
|
||||||
|
]"
|
||||||
|
:style="containerStyle"
|
||||||
|
@mouseenter="hoverIntensity ? (isHovered = true) : null"
|
||||||
|
@mouseleave="hoverIntensity ? (isHovered = false) : null"
|
||||||
|
>
|
||||||
|
<div class="relative w-full h-full">
|
||||||
|
<div v-for="(div, index) in blurDivs" :key="index" class="absolute inset-0" :style="div.style" />
|
||||||
|
</div>
|
||||||
|
<div v-if="$slots.default" class="relative">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
251
src/demo/Animations/GradualBlurDemo.vue
Normal file
251
src/demo/Animations/GradualBlurDemo.vue
Normal file
@@ -0,0 +1,251 @@
|
|||||||
|
<template>
|
||||||
|
<TabbedLayout>
|
||||||
|
<template #preview>
|
||||||
|
<div class="relative flex-col h-[500px] overflow-hidden demo-container demo-container-dots">
|
||||||
|
<div
|
||||||
|
ref="scrollRef"
|
||||||
|
class="relative flex flex-col items-center px-6 py-[100px] w-full h-full overflow-x-hidden overflow-y-auto scrollContainer"
|
||||||
|
>
|
||||||
|
<p class="z-0 font-bold text-[#9EF2AC] text-[clamp(2rem,4vw,5rem)]">Scroll Down.</p>
|
||||||
|
<img
|
||||||
|
src="https://images.unsplash.com/photo-1708778002586-37c4f23b7f3f?q=80&w=774&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
|
||||||
|
class="brightness-200 grayscale-0 my-[100px] border border-[#1E3921] rounded-[50px] w-full max-w-[600px] filter"
|
||||||
|
alt="Lighthouse in the distance with green colors."
|
||||||
|
/>
|
||||||
|
<p class="z-0 font-bold text-[#9EF2AC] text-[clamp(2rem,4vw,5rem)]">Gradual Blur</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<GradualBlur
|
||||||
|
v-bind="blurProps"
|
||||||
|
:style="{
|
||||||
|
zIndex: 10,
|
||||||
|
width: blurProps.position === 'left' || blurProps.position === 'right' ? '8rem' : '100%',
|
||||||
|
height: blurProps.position === 'top' || blurProps.position === 'bottom' ? blurProps.height : '100%'
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Customize>
|
||||||
|
<PreviewSelect title="Position" v-model="blurProps.position" :options="positionOptions" />
|
||||||
|
<PreviewSwitch title="Exponential" v-model="blurProps.exponential" />
|
||||||
|
<PreviewSlider title="Strength" :min="1" :max="5" :step="0.5" v-model="blurProps.strength" />
|
||||||
|
<PreviewSlider title="Div Count" :min="1" :max="10" :step="1" v-model="blurProps.divCount" />
|
||||||
|
<PreviewSlider title="Opacity" :min="0.1" :max="1" :step="0.1" v-model="blurProps.opacity" />
|
||||||
|
</Customize>
|
||||||
|
|
||||||
|
<PropTable :data="propData" />
|
||||||
|
<Dependencies :dependency-list="['mathjs']" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #code>
|
||||||
|
<CodeExample :code-object="gradualBlur" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #cli>
|
||||||
|
<CliInstallation :command="gradualBlur.cli" />
|
||||||
|
</template>
|
||||||
|
</TabbedLayout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { gradualBlur } from '@/constants/code/Animations/gradualBlurCode';
|
||||||
|
import Lenis from 'lenis';
|
||||||
|
import { onBeforeMount, onMounted, reactive, useTemplateRef } 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 PreviewSelect from '../../components/common/PreviewSelect.vue';
|
||||||
|
import PreviewSlider from '../../components/common/PreviewSlider.vue';
|
||||||
|
import PreviewSwitch from '../../components/common/PreviewSwitch.vue';
|
||||||
|
import PropTable from '../../components/common/PropTable.vue';
|
||||||
|
import TabbedLayout from '../../components/common/TabbedLayout.vue';
|
||||||
|
import GradualBlur, { type GradualBlurProps } from '../../content/Animations/GradualBlur/GradualBlur.vue';
|
||||||
|
|
||||||
|
const positionOptions = [
|
||||||
|
{ label: 'Top', value: 'top' },
|
||||||
|
{ label: 'Bottom', value: 'bottom' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const blurProps = reactive<GradualBlurProps>({
|
||||||
|
position: 'bottom',
|
||||||
|
strength: 2,
|
||||||
|
height: '7rem',
|
||||||
|
divCount: 5,
|
||||||
|
curve: 'bezier',
|
||||||
|
target: 'parent',
|
||||||
|
exponential: true,
|
||||||
|
opacity: 1
|
||||||
|
});
|
||||||
|
|
||||||
|
const scrollRef = useTemplateRef('scrollRef');
|
||||||
|
|
||||||
|
let cleanup: (() => void) | null = null;
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const el = scrollRef.value;
|
||||||
|
if (!el) return;
|
||||||
|
|
||||||
|
const isReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||||
|
const isTouch = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
|
||||||
|
if (isTouch || isReducedMotion) return;
|
||||||
|
|
||||||
|
const lenis = new Lenis({
|
||||||
|
wrapper: el,
|
||||||
|
content: el.firstElementChild ?? el,
|
||||||
|
duration: 2,
|
||||||
|
smoothWheel: true,
|
||||||
|
touchMultiplier: 1.2,
|
||||||
|
wheelMultiplier: 1,
|
||||||
|
lerp: 0.1
|
||||||
|
});
|
||||||
|
|
||||||
|
let rafId: number | undefined;
|
||||||
|
const raf = (time: number) => {
|
||||||
|
lenis.raf(time);
|
||||||
|
rafId = requestAnimationFrame(raf);
|
||||||
|
};
|
||||||
|
rafId = requestAnimationFrame(raf);
|
||||||
|
|
||||||
|
cleanup = () => {
|
||||||
|
if (rafId) cancelAnimationFrame(rafId);
|
||||||
|
lenis.destroy();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeMount(() => {
|
||||||
|
cleanup?.();
|
||||||
|
});
|
||||||
|
|
||||||
|
const propData = [
|
||||||
|
{
|
||||||
|
name: 'position',
|
||||||
|
type: `"top" | "bottom" | "left" | "right"`,
|
||||||
|
default: `"bottom"`,
|
||||||
|
description: 'Edge to attach the blur overlay.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'strength',
|
||||||
|
type: 'number',
|
||||||
|
default: '2',
|
||||||
|
description: 'Base blur strength multiplier (affects each layer).'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'height',
|
||||||
|
type: 'string',
|
||||||
|
default: `"6rem"`,
|
||||||
|
description: 'Overlay height (for top / bottom positions).'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'width',
|
||||||
|
type: 'string',
|
||||||
|
default: '—',
|
||||||
|
description:
|
||||||
|
'Custom width (optional). Defaults to 100% for vertical positions or matches height for horizontal positions.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'divCount',
|
||||||
|
type: 'number',
|
||||||
|
default: '5',
|
||||||
|
description: 'Number of stacked blur layers (higher = smoother gradient).'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'exponential',
|
||||||
|
type: 'boolean',
|
||||||
|
default: 'false',
|
||||||
|
description: 'Use exponential progression for stronger end blur.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'curve',
|
||||||
|
type: `"linear" | "bezier" | "ease-in"`,
|
||||||
|
default: `"linear"`,
|
||||||
|
description: 'Distribution curve applied to layer progression.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'opacity',
|
||||||
|
type: 'number',
|
||||||
|
default: '1',
|
||||||
|
description: 'Opacity applied to each blur layer.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'animated',
|
||||||
|
type: `"boolean" | "scroll"`,
|
||||||
|
default: 'false',
|
||||||
|
description: 'Fade in (true) or reveal on scroll ("scroll").'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'duration',
|
||||||
|
type: 'string',
|
||||||
|
default: `"0.3s"`,
|
||||||
|
description: 'Animation duration (when animated).'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'easing',
|
||||||
|
type: 'string',
|
||||||
|
default: `"ease-out"`,
|
||||||
|
description: 'Animation easing (opacity / backdrop-filter).'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'hoverIntensity',
|
||||||
|
type: 'number',
|
||||||
|
default: '—',
|
||||||
|
description: 'Multiplier applied to strength while hovered.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'target',
|
||||||
|
type: `"parent" | "page"`,
|
||||||
|
default: `"parent"`,
|
||||||
|
description: 'Position relative to parent container or the entire page (fixed).'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'preset',
|
||||||
|
type: `"top" | "bottom" | "left" | "right"`,
|
||||||
|
default: '—',
|
||||||
|
description: 'Apply a predefined configuration bundle.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'responsive',
|
||||||
|
type: 'boolean',
|
||||||
|
default: 'false',
|
||||||
|
description: 'Enable internal responsive recalculation (experimental).'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'zIndex',
|
||||||
|
type: 'number',
|
||||||
|
default: '1000',
|
||||||
|
description: 'Base z-index (page target adds +100).'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'onAnimationComplete',
|
||||||
|
type: '() => void',
|
||||||
|
default: '—',
|
||||||
|
description: 'Callback fired after animated reveal completes.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'className',
|
||||||
|
type: 'string',
|
||||||
|
default: '—',
|
||||||
|
description: 'Additional class names appended to root element.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'style',
|
||||||
|
type: 'CSSProperties',
|
||||||
|
default: '—',
|
||||||
|
description: 'Inline style overrides merged into container style.'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.demo-container {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrollContainer {
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
scrollbar-width: none;
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user