Files
vue-bits/public/r/GradualBlur.json
David Haz e621971723 jsrepo v3
2025-12-15 23:50:24 +02:00

1 line
11 KiB
JSON

{"name":"GradualBlur","title":"GradualBlur","description":"Progressively un-blurs content based on scroll or trigger creating a cinematic reveal.","type":"registry:component","add":"when-added","files":[{"type":"registry:component","role":"file","content":"<script setup lang=\"ts\">\nimport * as math from 'mathjs';\nimport { computed, onMounted, onUnmounted, ref, watch, type CSSProperties, type StyleValue } from 'vue';\n\nexport type GradualBlurProps = {\n position?: 'top' | 'bottom' | 'left' | 'right';\n strength?: number;\n height?: string;\n width?: string;\n divCount?: number;\n exponential?: boolean;\n zIndex?: number;\n animated?: boolean | 'scroll';\n duration?: string;\n easing?: string;\n opacity?: number;\n curve?: 'linear' | 'bezier' | 'ease-in' | 'ease-out' | 'ease-in-out';\n responsive?: boolean;\n mobileHeight?: string;\n tabletHeight?: string;\n desktopHeight?: string;\n mobileWidth?: string;\n tabletWidth?: string;\n desktopWidth?: string;\n preset?:\n | 'top'\n | 'bottom'\n | 'left'\n | 'right'\n | 'subtle'\n | 'intense'\n | 'smooth'\n | 'sharp'\n | 'header'\n | 'footer'\n | 'sidebar'\n | 'page-header'\n | 'page-footer';\n gpuOptimized?: boolean;\n hoverIntensity?: number;\n target?: 'parent' | 'page';\n onAnimationComplete?: () => void;\n className?: string;\n style?: CSSProperties;\n};\n\nconst props = withDefaults(defineProps<GradualBlurProps>(), {\n position: 'bottom',\n strength: 2,\n height: '6rem',\n divCount: 5,\n exponential: false,\n zIndex: 1000,\n animated: false,\n duration: '0.3s',\n easing: 'ease-out',\n opacity: 1,\n curve: 'linear',\n responsive: false,\n target: 'parent',\n className: ''\n});\n\nconst DEFAULT_CONFIG: Partial<GradualBlurProps> = {\n position: 'bottom',\n strength: 2,\n height: '6rem',\n divCount: 5,\n exponential: false,\n zIndex: 1000,\n animated: false,\n duration: '0.3s',\n easing: 'ease-out',\n opacity: 1,\n curve: 'linear',\n responsive: false,\n target: 'parent',\n className: '',\n style: {}\n};\n\nconst PRESETS: Record<string, Partial<GradualBlurProps>> = {\n top: { position: 'top', height: '6rem' },\n bottom: { position: 'bottom', height: '6rem' },\n left: { position: 'left', height: '6rem' },\n right: { position: 'right', height: '6rem' },\n\n subtle: { height: '4rem', strength: 1, opacity: 0.8, divCount: 3 },\n intense: { height: '10rem', strength: 4, divCount: 8, exponential: true },\n\n smooth: { height: '8rem', curve: 'bezier', divCount: 10 },\n sharp: { height: '5rem', curve: 'linear', divCount: 4 },\n\n header: { position: 'top', height: '8rem', curve: 'ease-out' },\n footer: { position: 'bottom', height: '8rem', curve: 'ease-out' },\n sidebar: { position: 'left', height: '6rem', strength: 2.5 },\n\n 'page-header': {\n position: 'top',\n height: '10rem',\n target: 'page',\n strength: 3\n },\n 'page-footer': {\n position: 'bottom',\n height: '10rem',\n target: 'page',\n strength: 3\n }\n};\n\nconst CURVE_FUNCTIONS: Record<string, (p: number) => number> = {\n linear: p => p,\n bezier: p => p * p * (3 - 2 * p),\n 'ease-in': p => p * p,\n 'ease-out': p => 1 - Math.pow(1 - p, 2),\n 'ease-in-out': p => (p < 0.5 ? 2 * p * p : 1 - Math.pow(-2 * p + 2, 2) / 2)\n};\n\nconst containerRef = ref<HTMLDivElement | null>(null);\nconst isHovered = ref(false);\nconst isVisible = ref(true);\nconst responsiveHeight = ref(props.height);\nconst responsiveWidth = ref(props.width);\n\nconst config = computed(() => {\n const presetConfig = props.preset && PRESETS[props.preset] ? PRESETS[props.preset] : {};\n return {\n ...DEFAULT_CONFIG,\n ...presetConfig,\n ...props\n } as Required<GradualBlurProps>;\n});\n\nconst getGradientDirection = (position: string): string => {\n const directions: Record<string, string> = {\n top: 'to top',\n bottom: 'to bottom',\n left: 'to left',\n right: 'to right'\n };\n return directions[position] || 'to bottom';\n};\n\nconst debounce = <T extends (...a: unknown[]) => void>(fn: T, wait: number) => {\n let timeout: ReturnType<typeof setTimeout>;\n return (...args: Parameters<T>) => {\n clearTimeout(timeout);\n timeout = setTimeout(() => fn(...args), wait);\n };\n};\n\nconst updateResponsiveDimensions = () => {\n if (!config.value.responsive) return;\n\n const width = window.innerWidth;\n const currentConfig = config.value;\n\n let newHeight = currentConfig.height;\n if (width <= 480 && currentConfig.mobileHeight) {\n newHeight = currentConfig.mobileHeight;\n } else if (width <= 768 && currentConfig.tabletHeight) {\n newHeight = currentConfig.tabletHeight;\n } else if (width <= 1024 && currentConfig.desktopHeight) {\n newHeight = currentConfig.desktopHeight;\n }\n responsiveHeight.value = newHeight;\n\n let newWidth = currentConfig.width;\n if (width <= 480 && currentConfig.mobileWidth) {\n newWidth = currentConfig.mobileWidth;\n } else if (width <= 768 && currentConfig.tabletWidth) {\n newWidth = currentConfig.tabletWidth;\n } else if (width <= 1024 && currentConfig.desktopWidth) {\n newWidth = currentConfig.desktopWidth;\n }\n responsiveWidth.value = newWidth;\n};\n\nlet intersectionObserver: IntersectionObserver | null = null;\n\nconst setupIntersectionObserver = () => {\n if (config.value.animated !== 'scroll' || !containerRef.value) return;\n\n intersectionObserver = new IntersectionObserver(\n ([entry]) => {\n isVisible.value = entry.isIntersecting;\n },\n { threshold: 0.1 }\n );\n\n intersectionObserver.observe(containerRef.value);\n};\n\nconst blurDivs = computed(() => {\n const divs: Array<{ style: CSSProperties }> = [];\n const increment = 100 / config.value.divCount;\n const currentStrength =\n isHovered.value && config.value.hoverIntensity\n ? config.value.strength * config.value.hoverIntensity\n : config.value.strength;\n\n const curveFunc = CURVE_FUNCTIONS[config.value.curve] || CURVE_FUNCTIONS.linear;\n\n for (let i = 1; i <= config.value.divCount; i++) {\n let progress = i / config.value.divCount;\n progress = curveFunc(progress);\n\n let blurValue: number;\n if (config.value.exponential) {\n blurValue = Number(math.pow(2, progress * 4)) * 0.0625 * currentStrength;\n } else {\n blurValue = 0.0625 * (progress * config.value.divCount + 1) * currentStrength;\n }\n\n const p1 = math.round((increment * i - increment) * 10) / 10;\n const p2 = math.round(increment * i * 10) / 10;\n const p3 = math.round((increment * i + increment) * 10) / 10;\n const p4 = math.round((increment * i + increment * 2) * 10) / 10;\n\n let gradient = `transparent ${p1}%, black ${p2}%`;\n if (p3 <= 100) gradient += `, black ${p3}%`;\n if (p4 <= 100) gradient += `, transparent ${p4}%`;\n\n const direction = getGradientDirection(config.value.position);\n\n const divStyle: CSSProperties = {\n maskImage: `linear-gradient(${direction}, ${gradient})`,\n WebkitMaskImage: `linear-gradient(${direction}, ${gradient})`,\n backdropFilter: `blur(${blurValue.toFixed(3)}rem)`,\n opacity: config.value.opacity,\n transition:\n config.value.animated && config.value.animated !== 'scroll'\n ? `backdrop-filter ${config.value.duration} ${config.value.easing}`\n : undefined\n };\n\n divs.push({ style: divStyle });\n }\n\n return divs;\n});\n\nconst containerStyle = computed((): StyleValue => {\n const isVertical = ['top', 'bottom'].includes(config.value.position);\n const isHorizontal = ['left', 'right'].includes(config.value.position);\n const isPageTarget = config.value.target === 'page';\n\n const baseStyle: CSSProperties = {\n position: isPageTarget ? 'fixed' : 'absolute',\n pointerEvents: config.value.hoverIntensity ? 'auto' : 'none',\n opacity: isVisible.value ? 1 : 0,\n transition: config.value.animated ? `opacity ${config.value.duration} ${config.value.easing}` : undefined,\n zIndex: isPageTarget ? config.value.zIndex + 100 : config.value.zIndex,\n ...config.value.style\n };\n\n if (isVertical) {\n baseStyle.height = responsiveHeight.value;\n baseStyle.width = responsiveWidth.value || '100%';\n baseStyle[config.value.position] = '0';\n baseStyle.left = '0';\n baseStyle.right = '0';\n } else if (isHorizontal) {\n baseStyle.width = responsiveWidth.value || responsiveHeight.value;\n baseStyle.height = '100%';\n baseStyle[config.value.position] = '0';\n baseStyle.top = '0';\n baseStyle.bottom = '0';\n }\n\n return baseStyle;\n});\n\nconst debouncedResize = debounce(updateResponsiveDimensions, 100);\n\nonMounted(() => {\n // Initialize responsive dimensions\n if (config.value.responsive) {\n updateResponsiveDimensions();\n window.addEventListener('resize', debouncedResize);\n }\n\n if (config.value.animated === 'scroll') {\n isVisible.value = false;\n setupIntersectionObserver();\n }\n\n injectStyles();\n});\n\nonUnmounted(() => {\n if (config.value.responsive) {\n window.removeEventListener('resize', debouncedResize);\n }\n\n if (intersectionObserver) {\n intersectionObserver.disconnect();\n }\n});\n\nwatch(\n () => isVisible.value,\n newVisible => {\n if (newVisible && config.value.animated === 'scroll' && props.onAnimationComplete) {\n const timeout = setTimeout(\n () => {\n if (props.onAnimationComplete) {\n props.onAnimationComplete();\n }\n },\n parseFloat(config.value.duration) * 1000\n );\n\n return () => clearTimeout(timeout);\n }\n }\n);\n\nconst injectStyles = () => {\n if (typeof document === 'undefined') return;\n const id = 'gradual-blur-styles';\n if (document.getElementById(id)) return;\n const el = document.createElement('style');\n el.id = id;\n el.textContent = `.gradual-blur{pointer-events:none;transition:opacity .3s ease-out}.gradual-blur-inner{pointer-events:none}`;\n document.head.appendChild(el);\n};\n</script>\n\n<template>\n <div\n ref=\"containerRef\"\n :class=\"[\n 'gradual-blur relative isolate',\n config.target === 'page' ? 'gradual-blur-page' : 'gradual-blur-parent',\n config.className\n ]\"\n :style=\"containerStyle\"\n @mouseenter=\"hoverIntensity ? (isHovered = true) : null\"\n @mouseleave=\"hoverIntensity ? (isHovered = false) : null\"\n >\n <div class=\"relative w-full h-full\">\n <div v-for=\"(div, index) in blurDivs\" :key=\"index\" class=\"absolute inset-0\" :style=\"div.style\" />\n </div>\n <div v-if=\"$slots.default\" class=\"relative\">\n <slot />\n </div>\n </div>\n</template>\n","path":"GradualBlur/GradualBlur.vue","_imports_":[],"registryDependencies":[],"dependencies":[],"devDependencies":[]}],"registryDependencies":[],"dependencies":[{"ecosystem":"js","name":"mathjs","version":"^14.6.0"}],"devDependencies":[],"categories":["Animations"]}