diff --git a/package-lock.json b/package-lock.json index bb337347..2a101c0b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "react-bits", "version": "0.0.0", "dependencies": { + "@chakra-ui/icons": "^2.2.4", "@chakra-ui/react": "^3.20.0", "@emotion/react": "^11.14.0", "@gsap/react": "^2.1.2", @@ -20,6 +21,7 @@ "gsap": "^3.13.0", "lenis": "^1.3.5", "maath": "^0.10.8", + "mathjs": "^14.6.0", "matter-js": "^0.20.0", "meshline": "^3.3.1", "motion": "^12.23.12", @@ -472,6 +474,16 @@ "integrity": "sha512-ZqNlhKcZW6MW1LxWIOfh9YVrBykvzyFad3bOh6JJFraDnNa3NXboRDiaI8dmrbb0ZHXCU1Tsq6WQsKV2Vpp5dw==", "dev": true }, + "node_modules/@chakra-ui/icons": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@chakra-ui/icons/-/icons-2.2.4.tgz", + "integrity": "sha512-l5QdBgwrAg3Sc2BRqtNkJpfuLw/pWRDwwT58J6c4PqQT6wzXxyNa8Q0PForu1ltB5qEiFb1kxr/F/HO1EwNa6g==", + "license": "MIT", + "peerDependencies": { + "@chakra-ui/react": ">=2.0.0", + "react": ">=18" + } + }, "node_modules/@chakra-ui/react": { "version": "3.20.0", "resolved": "https://registry.npmjs.org/@chakra-ui/react/-/react-3.20.0.tgz", @@ -4011,7 +4023,7 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "devOptional": true, + "dev": true, "dependencies": { "fill-range": "^7.1.1" }, @@ -4342,6 +4354,19 @@ "node": ">=18" } }, + "node_modules/complex.js": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/complex.js/-/complex.js-2.4.2.tgz", + "integrity": "sha512-qtx7HRhPGSCBtGiST4/WGHuW+zeaND/6Ld+db6PbrulIB1i2Ev/2UPiqcmpQNPSyfBKraC0EOvOKCB5dGZKt3g==", + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -4632,6 +4657,12 @@ } } }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "license": "MIT" + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -5037,6 +5068,12 @@ "node": ">=6" } }, + "node_modules/escape-latex": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/escape-latex/-/escape-latex-1.2.0.tgz", + "integrity": "sha512-nV5aVWW1K0wEiUIEdZ4erkGGH8mDxGyxSeqPzRNtWP7ataw+/olFObw7hujFWlVjNsaDFw5VZ5NzVSIqRgfTiw==", + "license": "MIT" + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -5538,7 +5575,7 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "devOptional": true, + "dev": true, "dependencies": { "to-regex-range": "^5.0.1" }, @@ -5662,6 +5699,19 @@ "node": ">=12.20.0" } }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, "node_modules/framer-motion": { "version": "12.23.12", "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.12.tgz", @@ -6362,7 +6412,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "devOptional": true, + "dev": true, "engines": { "node": ">=0.10.0" } @@ -6407,7 +6457,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "devOptional": true, + "dev": true, "dependencies": { "is-extglob": "^2.1.1" }, @@ -6453,7 +6503,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "devOptional": true, + "dev": true, "engines": { "node": ">=0.12.0" } @@ -6697,6 +6747,12 @@ "react": "^19.0.0" } }, + "node_modules/javascript-natural-sort": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz", + "integrity": "sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw==", + "license": "MIT" + }, "node_modules/jiti": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", @@ -7266,6 +7322,29 @@ "node": ">= 0.4" } }, + "node_modules/mathjs": { + "version": "14.6.0", + "resolved": "https://registry.npmjs.org/mathjs/-/mathjs-14.6.0.tgz", + "integrity": "sha512-5vI2BLB5GKQmiSK9BH6hVkZ+GgqpdnOgEfmHl7mqVmdQObLynr63KueyYYLCQMzj66q69mV2XZZGQqqxeftQbA==", + "license": "Apache-2.0", + "dependencies": { + "@babel/runtime": "^7.26.10", + "complex.js": "^2.2.5", + "decimal.js": "^10.4.3", + "escape-latex": "^1.2.0", + "fraction.js": "^5.2.1", + "javascript-natural-sort": "^0.7.1", + "seedrandom": "^3.0.5", + "tiny-emitter": "^2.1.0", + "typed-function": "^4.2.1" + }, + "bin": { + "mathjs": "bin/cli.js" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/matter-js": { "version": "0.20.0", "resolved": "https://registry.npmjs.org/matter-js/-/matter-js-0.20.0.tgz", @@ -7898,7 +7977,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "devOptional": true, + "dev": true, "engines": { "node": ">=8.6" }, @@ -8526,6 +8605,12 @@ "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", "license": "MIT" }, + "node_modules/seedrandom": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz", + "integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==", + "license": "MIT" + }, "node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -8990,11 +9075,17 @@ "integrity": "sha512-IQrh3lEPM93wVCEczc9SaAOvkmcoQn/G8Bo1e8ZPlY3X3bnAxWaBdvTdvM1hP62iZp0BXWDy4vTAy4fF0+Dlpg==", "license": "MIT" }, + "node_modules/tiny-emitter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz", + "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==", + "license": "MIT" + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "devOptional": true, + "dev": true, "dependencies": { "is-number": "^7.0.0" }, @@ -9232,6 +9323,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/typed-function": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/typed-function/-/typed-function-4.2.1.tgz", + "integrity": "sha512-EGjWssW7Tsk4DGfE+5yluuljS1OGYWiI1J6e8puZz9nTMM51Oug8CD5Zo4gWMsOhq5BI+1bF+rWTm4Vbj3ivRA==", + "license": "MIT", + "engines": { + "node": ">= 18" + } + }, "node_modules/typescript": { "version": "5.7.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", diff --git a/package.json b/package.json index 18c1d275..1b97ecc4 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "preview": "vite preview" }, "dependencies": { + "@chakra-ui/icons": "^2.2.4", "@chakra-ui/react": "^3.20.0", "@emotion/react": "^11.14.0", "@gsap/react": "^2.1.2", @@ -28,6 +29,7 @@ "gsap": "^3.13.0", "lenis": "^1.3.5", "maath": "^0.10.8", + "mathjs": "^14.6.0", "matter-js": "^0.20.0", "meshline": "^3.3.1", "motion": "^12.23.12", diff --git a/src/constants/Categories.js b/src/constants/Categories.js index b54f7d3a..0d72660e 100644 --- a/src/constants/Categories.js +++ b/src/constants/Categories.js @@ -1,5 +1,5 @@ // Highlighted sidebar items -export const NEW = ['Prismatic Burst', 'Gradient Blinds', 'Bubble Menu', 'Electric Border', 'Plasma', 'Prism', 'Logo Loop', 'Card Nav', 'Pill Nav', 'Target Cursor']; +export const NEW = ['Gradual Blur', 'Prismatic Burst', 'Gradient Blinds', 'Bubble Menu', 'Electric Border', 'Plasma', 'Prism', 'Logo Loop', 'Card Nav', 'Pill Nav', 'Target Cursor']; export const UPDATED = []; // Used for main sidebar navigation @@ -43,6 +43,7 @@ export const CATEGORIES = [ { name: 'Animations', subcategories: [ + 'Gradual Blur', 'Animated Content', 'Fade Content', 'Electric Border', diff --git a/src/constants/Components.js b/src/constants/Components.js index 4a413f07..8c3c4b3d 100644 --- a/src/constants/Components.js +++ b/src/constants/Components.js @@ -5,6 +5,7 @@ const getStarted = { } const animations = { + 'gradual-blur': () => import("../demo/Animations/GradualBlurDemo"), 'blob-cursor': () => import("../demo/Animations/BlobCursorDemo"), 'animated-content': () => import("../demo/Animations/AnimatedContentDemo"), 'magnet': () => import("../demo/Animations/MagnetDemo"), diff --git a/src/constants/code/Animations/gradualblurCode.js b/src/constants/code/Animations/gradualblurCode.js new file mode 100644 index 00000000..141e8cea --- /dev/null +++ b/src/constants/code/Animations/gradualblurCode.js @@ -0,0 +1,33 @@ +import { generateCliCommands } from '@/utils/utils'; + +import code from '@content/Animations/GradualBlur/GradualBlur.jsx?raw'; +import tailwind from '@tailwind/Animations/GradualBlur/GradualBlur.jsx?raw'; +import tsCode from '@ts-default/Animations/GradualBlur/GradualBlur.tsx?raw'; +import tsTailwind from '@ts-tailwind/Animations/GradualBlur/GradualBlur.tsx?raw'; + +export const gradualBlur = { + ...(generateCliCommands('Animations/GradualBlur')), + Installation: `npm install gradualblur mathjs`, + usage: ` + +import GradualBlur from 'gradualblur' + + +
+

+

+
+
`, + code, + tailwind, + tsCode, + tsTailwind +} diff --git a/src/content/Animations/GradualBlur/GradualBlur.css b/src/content/Animations/GradualBlur/GradualBlur.css new file mode 100644 index 00000000..663c9126 --- /dev/null +++ b/src/content/Animations/GradualBlur/GradualBlur.css @@ -0,0 +1,35 @@ +.gradual-blur-inner { + position: relative; + width: 100%; + height: 100%; +} + +/* Ensure backdrop-filter works with proper browser prefixes */ +.gradual-blur-inner > div { + -webkit-backdrop-filter: inherit; + backdrop-filter: inherit; +} + +/* Ensure proper rendering for the blur effect */ +.gradual-blur { + isolation: isolate; +} + +/* Fallback for browsers that don't support backdrop-filter */ +@supports not (backdrop-filter: blur(1px)) { + .gradual-blur-inner > div { + background: rgba(0, 0, 0, 0.3); + opacity: 0.5; + } +} + +/* Fix for parent container positioning */ +.gradual-blur-fixed { + position: fixed !important; + top: 0; + left: 0; + right: 0; + bottom: 0; + pointer-events: none; + z-index: 1000; +} diff --git a/src/content/Animations/GradualBlur/GradualBlur.jsx b/src/content/Animations/GradualBlur/GradualBlur.jsx new file mode 100644 index 00000000..dfa871dd --- /dev/null +++ b/src/content/Animations/GradualBlur/GradualBlur.jsx @@ -0,0 +1,387 @@ +import React, { useEffect, useRef, useState, useMemo, useCallback } from 'react'; +import * as math from 'mathjs'; + +// Default configuration - simplified and clean +const DEFAULT_CONFIG = { + 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', // NEW: 'parent' | 'page' + className: '', + style: {} +}; + +// Streamlined presets - height-only approach +const PRESETS = { + // Basic positions + top: { position: 'top', height: '6rem' }, + bottom: { position: 'bottom', height: '6rem' }, + left: { position: 'left', height: '6rem' }, + right: { position: 'right', height: '6rem' }, + + // Intensity variations + subtle: { height: '4rem', strength: 1, opacity: 0.8, divCount: 3 }, + intense: { height: '10rem', strength: 4, divCount: 8, exponential: true }, + + // Style variations + smooth: { height: '8rem', curve: 'bezier', divCount: 10 }, + sharp: { height: '5rem', curve: 'linear', divCount: 4 }, + + // Common use cases + 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-level presets + 'page-header': { position: 'top', height: '10rem', target: 'page', strength: 3 }, + 'page-footer': { position: 'bottom', height: '10rem', target: 'page', strength: 3 } +}; + +// Curve functions - essential ones only +const CURVE_FUNCTIONS = { + linear: (progress) => progress, + bezier: (progress) => progress * progress * (3 - 2 * progress), + 'ease-in': (progress) => progress * progress, + 'ease-out': (progress) => 1 - Math.pow(1 - progress, 2), + 'ease-in-out': (progress) => + progress < 0.5 ? 2 * progress * progress : 1 - Math.pow(-2 * progress + 2, 2) / 2 +}; + +// Utility functions +const mergeConfigs = (...configs) => { + return configs.reduce((acc, config) => ({ ...acc, ...config }), {}); +}; + +const getGradientDirection = (position) => { + const directions = { + top: 'to top', + bottom: 'to bottom', + left: 'to left', + right: 'to right' + }; + return directions[position] || 'to bottom'; +}; + +const debounce = (func, wait) => { + let timeout; + return function executedFunction(...args) { + const later = () => { + clearTimeout(timeout); + func(...args); + }; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + }; +}; + +// Custom hooks +const useResponsiveHeight = (responsive, config) => { + const [height, setHeight] = useState(config.height); + + const updateHeight = useCallback(debounce(() => { + if (!responsive) return; + + const screenWidth = window.innerWidth; + let newHeight = config.height; + + if (screenWidth <= 480 && config.mobileHeight) { + newHeight = config.mobileHeight; + } else if (screenWidth <= 768 && config.tabletHeight) { + newHeight = config.tabletHeight; + } else if (screenWidth <= 1024 && config.desktopHeight) { + newHeight = config.desktopHeight; + } + + setHeight(newHeight); + }, 100), [responsive, config]); + + useEffect(() => { + if (!responsive) return; + + updateHeight(); + window.addEventListener('resize', updateHeight); + return () => window.removeEventListener('resize', updateHeight); + }, [responsive, updateHeight]); + + return responsive ? height : config.height; +}; + +const useResponsiveWidth = (responsive, config) => { + const [width, setWidth] = useState(config.width); + + const updateWidth = useCallback(debounce(() => { + if (!responsive) return; + + const screenWidth = window.innerWidth; + let newWidth = config.width; + + if (screenWidth <= 480 && config.mobileWidth) { + newWidth = config.mobileWidth; + } else if (screenWidth <= 768 && config.tabletWidth) { + newWidth = config.tabletWidth; + } else if (screenWidth <= 1024 && config.desktopWidth) { + newWidth = config.desktopWidth; + } + + setWidth(newWidth); + }, 100), [responsive, config]); + + useEffect(() => { + if (!responsive) return; + + updateWidth(); + window.addEventListener('resize', updateWidth); + return () => window.removeEventListener('resize', updateWidth); + }, [responsive, updateWidth]); + + return responsive ? width : config.width; +}; + +const useIntersectionObserver = (ref, shouldObserve = false) => { + const [isVisible, setIsVisible] = useState(!shouldObserve); + + useEffect(() => { + if (!shouldObserve || !ref.current) return; + + const observer = new IntersectionObserver( + ([entry]) => setIsVisible(entry.isIntersecting), + { threshold: 0.1 } + ); + + observer.observe(ref.current); + return () => observer.disconnect(); + }, [ref, shouldObserve]); + + return isVisible; +}; + +// Main component +const GradualBlur = (props) => { + const containerRef = useRef(null); + const [isHovered, setIsHovered] = useState(false); + + // Merge configurations + const config = useMemo(() => { + const presetConfig = props.preset && PRESETS[props.preset] ? PRESETS[props.preset] : {}; + return mergeConfigs(DEFAULT_CONFIG, presetConfig, props); + }, [props]); + + // Responsive height and width + const responsiveHeight = useResponsiveHeight(config.responsive, config); + const responsiveWidth = useResponsiveWidth(config.responsive, config); + + // Intersection observer for scroll animations + const isVisible = useIntersectionObserver(containerRef, config.animated === 'scroll'); + + // Memoized blur divs generation + const blurDivs = useMemo(() => { + const divs = []; + const increment = 100 / config.divCount; + const currentStrength = isHovered && config.hoverIntensity ? + config.strength * config.hoverIntensity : config.strength; + + const curveFunc = CURVE_FUNCTIONS[config.curve] || CURVE_FUNCTIONS.linear; + + for (let i = 1; i <= config.divCount; i++) { + let progress = i / config.divCount; + progress = curveFunc(progress); + + // Calculate blur value + let blurValue; + if (config.exponential) { + blurValue = math.pow(2, progress * 4) * 0.0625 * currentStrength; + } else { + blurValue = 0.0625 * (progress * config.divCount + 1) * currentStrength; + } + + // Calculate gradient positions + 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; + + // Generate gradient + let gradient = `transparent ${p1}%, black ${p2}%`; + if (p3 <= 100) gradient += `, black ${p3}%`; + if (p4 <= 100) gradient += `, transparent ${p4}%`; + + const direction = getGradientDirection(config.position); + + const divStyle = { + position: 'absolute', + inset: '0', + maskImage: `linear-gradient(${direction}, ${gradient})`, + WebkitMaskImage: `linear-gradient(${direction}, ${gradient})`, + backdropFilter: `blur(${blurValue.toFixed(3)}rem)`, + WebkitBackdropFilter: `blur(${blurValue.toFixed(3)}rem)`, + opacity: config.opacity, + transition: config.animated && config.animated !== 'scroll' ? + `backdrop-filter ${config.duration} ${config.easing}` : undefined + }; + + divs.push(
); + } + + return divs; + }, [config, isHovered]); + + // Container styles with target-aware positioning + const containerStyle = useMemo(() => { + const isVertical = ['top', 'bottom'].includes(config.position); + const isHorizontal = ['left', 'right'].includes(config.position); + const isPageTarget = config.target === 'page'; + + const baseStyle = { + // Position based on target + position: isPageTarget ? 'fixed' : 'absolute', + pointerEvents: config.hoverIntensity ? 'auto' : 'none', + opacity: isVisible ? 1 : 0, + transition: config.animated ? `opacity ${config.duration} ${config.easing}` : undefined, + zIndex: isPageTarget ? config.zIndex + 100 : config.zIndex, // Higher z-index for page targeting + ...config.style + }; + + // Apply dimensions and positioning + if (isVertical) { + baseStyle.height = responsiveHeight; + baseStyle.width = responsiveWidth || '100%'; + baseStyle[config.position] = 0; + baseStyle.left = 0; + baseStyle.right = 0; + } else if (isHorizontal) { + baseStyle.width = responsiveWidth || responsiveHeight; // Use width prop if provided, otherwise use height value + baseStyle.height = '100%'; + baseStyle[config.position] = 0; + baseStyle.top = 0; + baseStyle.bottom = 0; + } + + return baseStyle; + }, [config, responsiveHeight, responsiveWidth, isVisible]); + + // Event handlers + const handleMouseEnter = useCallback(() => { + if (config.hoverIntensity) { + setIsHovered(true); + } + }, [config.hoverIntensity]); + + const handleMouseLeave = useCallback(() => { + if (config.hoverIntensity) { + setIsHovered(false); + } + }, [config.hoverIntensity]); + + // Animation complete callback + useEffect(() => { + if (isVisible && config.animated === 'scroll' && config.onAnimationComplete) { + const timer = setTimeout( + () => config.onAnimationComplete(), + parseFloat(config.duration) * 1000 + ); + return () => clearTimeout(timer); + } + }, [isVisible, config.animated, config.duration, config.onAnimationComplete]); + + return ( +
+
+ {blurDivs} +
+
+ ); +}; + +// Factory function for creating instances with different targets +export const createPageBlur = (props = {}) => { + return ; +}; + +export const createParentBlur = (props = {}) => { + return ; +}; + +// Export utilities +export { PRESETS, CURVE_FUNCTIONS }; + +export default React.memo(GradualBlur); + +// CSS injection function +const injectStyles = () => { + if (typeof document === 'undefined') return; + + const styleId = 'gradual-blur-styles'; + if (document.getElementById(styleId)) return; + + const styleElement = document.createElement('style'); + styleElement.id = styleId; + styleElement.textContent = ` + .gradual-blur { + pointer-events: none; + } + + .gradual-blur-page { + /* Page-level blur styles */ + } + + .gradual-blur-parent { + /* Parent-level blur styles */ + } + + .gradual-blur-inner { + pointer-events: none; + } + + /* Hover support */ + .gradual-blur:hover .gradual-blur-inner { + /* Hover effects can be added here */ + } + + /* Animation support */ + .gradual-blur { + transition: opacity 0.3s ease-out; + } + + /* Responsive utilities */ + @media (max-width: 480px) { + .gradual-blur-responsive { + /* Mobile specific styles */ + } + } + + @media (max-width: 768px) { + .gradual-blur-responsive { + /* Tablet specific styles */ + } + } + `; + + document.head.appendChild(styleElement); +}; + +// Inject styles on component mount +if (typeof document !== 'undefined') { + injectStyles(); +} diff --git a/src/demo/Animations/GradualBlurDemo.jsx b/src/demo/Animations/GradualBlurDemo.jsx new file mode 100644 index 00000000..782012f8 --- /dev/null +++ b/src/demo/Animations/GradualBlurDemo.jsx @@ -0,0 +1,326 @@ +import { useState } from "react"; +import { + Box, + Heading, + Text, + VStack, + HStack, + SimpleGrid, + Code, + Image, + Link, + Button, +} from "@chakra-ui/react"; +import { + CodeTab, + PreviewTab, + CliTab, + TabsLayout, +} from "../../components/common/TabsLayout"; + +import Customize from "../../components/common/Preview/Customize"; +import CodeExample from "../../components/code/CodeExample"; +import CliInstallation from "../../components/code/CliInstallation"; +import PropTable from "../../components/common/Preview/PropTable"; +import PreviewSelect from "../../components/common/Preview/PreviewSelect"; +import PreviewSlider from "../../components/common/Preview/PreviewSlider"; + +import { gradualBlur } from "../../constants/code/Animations/gradualblurCode"; +import GradualBlur from "../../tailwind/Animations/GradualBlur/GradualBlur"; + +const GradualBlurDemo = () => { + const propData = [ + { + name: "position", + type: `"top" | "bottom | left | right"`, + default: `"bottom"`, + description: "Position of the blur overlay.", + }, + { + name: "strength", + type: "number", + default: "2", + description: "Overall blur strength multiplier.", + }, + { + name: "height", + type: "string", + default: `"7rem"`, + description: "Height of the blur region.", + }, + { + name: "width", + type: "string", + default: `"100%"`, + description: "Width of the blur region.", + }, + { + name: "divCount", + type: "number", + default: "5", + description: "Number of stacked blur layers.", + }, + { + name: "exponential", + type: "boolean", + default: "true", + description: "Use exponential blur progression.", + }, + { + name: "animated", + type: `"boolean" | "scroll"`, + default: "false", + description: "Enable animation or scroll-based reveal.", + }, + { + name: "duration", + type: "string", + default: `"0.3s"`, + description: "Animation duration.", + }, + { + name: "easing", + type: "string", + default: `"ease-out"`, + description: "Animation easing function.", + }, + { + name: "opacity", + type: "number", + default: "1", + description: "Layer opacity.", + }, + { + name: "curve", + type: `"linear" | "bezier" | "ease-in-out"`, + default: `"bezier"`, + description: "Controls blur progression curve.", + }, + { + name: "responsive", + type: "boolean", + default: "false", + description: "Enable responsive heights.", + }, + { + name: "preset", + type: `"top" | "bottom"`, + default: "—", + description: "Quickly apply a preset config.", + }, + { + name: "gpuOptimized", + type: "boolean", + default: "false", + description: "Enable GPU optimization (`will-change`).", + }, + { + name: "hoverIntensity", + type: "number", + default: "—", + description: "Increase blur strength on hover.", + }, + { + name: "target", + type: `"parent" | "page"`, + default: `"parent"`, + description: "Position relative to parent container or entire page.", + }, + { + name: "onAnimationComplete", + type: "() => void", + default: "—", + description: "Callback after animation finishes.", + }, + { + name: "className", + type: "string", + default: "—", + description: "Custom CSS class.", + }, + { + name: "style", + type: "React.CSSProperties", + default: "—", + description: "Inline style overrides.", + }, + ]; + + const [blurProps, setBlurProps] = useState({ + position: "bottom", + strength: 2, + height: "7rem", + divCount: 5, + curve: "bezier", + exponential: true, + opacity: 1, + }); + + return ( + + + + + + + + + + Scroll Down + + + + + A beautiful Flower + + + + + + A beautiful Flower + + + @Flower + + + + + + + + Gradual Blur + + + + + + setBlurProps((p) => ({ ...p, position: v }))} + /> + setBlurProps((p) => ({ ...p, curve: v }))} + /> + setBlurProps((p) => ({ ...p, exponential: v === "true" }))} + /> + setBlurProps((p) => ({ ...p, strength: v }))} + /> + setBlurProps((p) => ({ ...p, divCount: v }))} + /> + setBlurProps((p) => ({ ...p, opacity: v }))} + /> + + + + + + + + + + + + + + ); +}; + +export default GradualBlurDemo; diff --git a/src/tailwind/Animations/GradualBlur/GradualBlur.jsx b/src/tailwind/Animations/GradualBlur/GradualBlur.jsx new file mode 100644 index 00000000..6d50d7ff --- /dev/null +++ b/src/tailwind/Animations/GradualBlur/GradualBlur.jsx @@ -0,0 +1,395 @@ +import React, { + useEffect, + useRef, + useState, + useMemo, + useCallback, +} from "react"; +import * as math from "mathjs"; + +const DEFAULT_CONFIG = { + position: "bottom", + strength: 2, + height: "6rem", + divCount: 5, + exponential: true, + zIndex: 40, + animated: false, + duration: "0.3s", + easing: "ease-out", + opacity: 1, + curve: "bezier", + responsive: false, + target: "page", + className: "", + style: {}, +}; + +// Streamlined presets - height-only approach +const PRESETS = { + // Basic positions + top: { position: "top", height: "6rem" }, + bottom: { position: "bottom", height: "6rem" }, + left: { position: "left", height: "6rem" }, + right: { position: "right", height: "6rem" }, + + // Intensity variations + subtle: { height: "4rem", strength: 1, opacity: 0.8, divCount: 3 }, + intense: { height: "10rem", strength: 4, divCount: 8, exponential: true }, + + // Style variations + smooth: { height: "8rem", curve: "bezier", divCount: 10 }, + sharp: { height: "5rem", curve: "linear", divCount: 4 }, + + // Common use cases + 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-level presets + "page-header": { + position: "top", + height: "10rem", + target: "page", + strength: 3, + }, + "page-footer": { + position: "bottom", + height: "10rem", + target: "page", + strength: 3, + }, +}; + +// Curve functions - essential ones only +const CURVE_FUNCTIONS = { + linear: (progress) => progress, + bezier: (progress) => progress * progress * (3 - 2 * progress), + "ease-in": (progress) => progress * progress, + "ease-out": (progress) => 1 - Math.pow(1 - progress, 2), + "ease-in-out": (progress) => + progress < 0.5 + ? 2 * progress * progress + : 1 - Math.pow(-2 * progress + 2, 2) / 2, +}; + +// Utility functions +const mergeConfigs = (...configs) => { + return configs.reduce((acc, config) => ({ ...acc, ...config }), {}); +}; + +const getGradientDirection = (position) => { + const directions = { + top: "to top", + bottom: "to bottom", + left: "to left", + right: "to right", + }; + return directions[position] || "to bottom"; +}; + +const debounce = (func, wait) => { + let timeout; + return function executedFunction(...args) { + const later = () => { + clearTimeout(timeout); + func(...args); + }; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + }; +}; + +// Custom hooks +const useResponsiveHeight = (responsive, config) => { + const [height, setHeight] = useState(config.height); + + const updateHeight = useCallback( + debounce(() => { + if (!responsive) return; + + const screenWidth = window.innerWidth; + let newHeight = config.height; + + if (screenWidth <= 480 && config.mobileHeight) { + newHeight = config.mobileHeight; + } else if (screenWidth <= 768 && config.tabletHeight) { + newHeight = config.tabletHeight; + } else if (screenWidth <= 1024 && config.desktopHeight) { + newHeight = config.desktopHeight; + } + + setHeight(newHeight); + }, 100), + [responsive, config] + ); + + useEffect(() => { + if (!responsive) return; + + updateHeight(); + window.addEventListener("resize", updateHeight); + return () => window.removeEventListener("resize", updateHeight); + }, [responsive, updateHeight]); + + return responsive ? height : config.height; +}; + +const useResponsiveWidth = (responsive, config) => { + const [width, setWidth] = useState(config.width); + + const updateWidth = useCallback( + debounce(() => { + if (!responsive) return; + + const screenWidth = window.innerWidth; + let newWidth = config.width; + + if (screenWidth <= 480 && config.mobileWidth) { + newWidth = config.mobileWidth; + } else if (screenWidth <= 768 && config.tabletWidth) { + newWidth = config.tabletWidth; + } else if (screenWidth <= 1024 && config.desktopWidth) { + newWidth = config.desktopWidth; + } + + setWidth(newWidth); + }, 100), + [responsive, config] + ); + + useEffect(() => { + if (!responsive) return; + + updateWidth(); + window.addEventListener("resize", updateWidth); + return () => window.removeEventListener("resize", updateWidth); + }, [responsive, updateWidth]); + + return responsive ? width : config.width; +}; + +const useIntersectionObserver = (ref, shouldObserve = false) => { + const [isVisible, setIsVisible] = useState(!shouldObserve); + + useEffect(() => { + if (!shouldObserve || !ref.current) return; + + const observer = new IntersectionObserver( + ([entry]) => setIsVisible(entry.isIntersecting), + { threshold: 0.1 } + ); + + observer.observe(ref.current); + return () => observer.disconnect(); + }, [ref, shouldObserve]); + + return isVisible; +}; + +// Main component +const GradualBlur = (props) => { + const containerRef = useRef(null); + const [isHovered, setIsHovered] = useState(false); + + // Merge configurations + const config = useMemo(() => { + const presetConfig = + props.preset && PRESETS[props.preset] ? PRESETS[props.preset] : {}; + return mergeConfigs(DEFAULT_CONFIG, presetConfig, props); + }, [props]); + + // Responsive height and width + const responsiveHeight = useResponsiveHeight(config.responsive, config); + const responsiveWidth = useResponsiveWidth(config.responsive, config); + + // Intersection observer for scroll animations + const isVisible = useIntersectionObserver( + containerRef, + config.animated === "scroll" + ); + + // Memoized blur divs generation + const blurDivs = useMemo(() => { + const divs = []; + const increment = 100 / config.divCount; + const currentStrength = + isHovered && config.hoverIntensity + ? config.strength * config.hoverIntensity + : config.strength; + + const curveFunc = CURVE_FUNCTIONS[config.curve] || CURVE_FUNCTIONS.linear; + + for (let i = 1; i <= config.divCount; i++) { + let progress = i / config.divCount; + progress = curveFunc(progress); + + // Calculate blur value + let blurValue; + if (config.exponential) { + blurValue = math.pow(2, progress * 4) * 0.0625 * currentStrength; + } else { + blurValue = 0.0625 * (progress * config.divCount + 1) * currentStrength; + } + + // Calculate gradient positions + 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; + + // Generate gradient + let gradient = `transparent ${p1}%, black ${p2}%`; + if (p3 <= 100) gradient += `, black ${p3}%`; + if (p4 <= 100) gradient += `, transparent ${p4}%`; + + const direction = getGradientDirection(config.position); + + const divStyle = { + position: "absolute", + inset: "0", + maskImage: `linear-gradient(${direction}, ${gradient})`, + WebkitMaskImage: `linear-gradient(${direction}, ${gradient})`, + backdropFilter: `blur(${blurValue.toFixed(3)}rem)`, + WebkitBackdropFilter: `blur(${blurValue.toFixed(3)}rem)`, + opacity: config.opacity, + transition: + config.animated && config.animated !== "scroll" + ? `backdrop-filter ${config.duration} ${config.easing}` + : undefined, + }; + + divs.push(
); + } + + return divs; + }, [config, isHovered]); + + // Container styles with target-aware positioning + const containerStyle = useMemo(() => { + const isVertical = ["top", "bottom"].includes(config.position); + const isHorizontal = ["left", "right"].includes(config.position); + const isPageTarget = config.target === "page"; + + const baseStyle = { + // Position based on target + position: isPageTarget ? "fixed" : "absolute", + pointerEvents: config.hoverIntensity ? "auto" : "none", + opacity: isVisible ? 1 : 0, + transition: config.animated + ? `opacity ${config.duration} ${config.easing}` + : undefined, + zIndex: isPageTarget ? config.zIndex + 100 : config.zIndex, + ...config.style, + }; + + // Apply dimensions and positioning + if (isVertical) { + baseStyle.height = responsiveHeight; + baseStyle.width = responsiveWidth || "100%"; + baseStyle[config.position] = 0; + baseStyle.left = 0; + baseStyle.right = 0; + } else if (isHorizontal) { + baseStyle.width = responsiveWidth || responsiveHeight; + baseStyle.height = "100%"; + baseStyle[config.position] = 0; + baseStyle.top = 0; + baseStyle.bottom = 0; + } + + return baseStyle; + }, [config, responsiveHeight, responsiveWidth, isVisible]); + + // Event handlers + const handleMouseEnter = useCallback(() => { + if (config.hoverIntensity) { + setIsHovered(true); + } + }, [config.hoverIntensity]); + + const handleMouseLeave = useCallback(() => { + if (config.hoverIntensity) { + setIsHovered(false); + } + }, [config.hoverIntensity]); + + // Animation complete callback + useEffect(() => { + if ( + isVisible && + config.animated === "scroll" && + config.onAnimationComplete + ) { + const timer = setTimeout( + () => config.onAnimationComplete(), + parseFloat(config.duration) * 1000 + ); + return () => clearTimeout(timer); + } + }, [isVisible, config.animated, config.duration, config.onAnimationComplete]); + + return ( +
+
+ {blurDivs} +
+
+ ); +}; + +// Factory function for creating instances with different targets +export const createPageBlur = (props = {}) => { + return ; +}; + +export const createParentBlur = (props = {}) => { + return ; +}; + +// Export utilities +export { PRESETS, CURVE_FUNCTIONS }; + +export default React.memo(GradualBlur); diff --git a/src/ts-default/Animations/GradualBlur/GradualBlur.css b/src/ts-default/Animations/GradualBlur/GradualBlur.css new file mode 100644 index 00000000..d57b0bdc --- /dev/null +++ b/src/ts-default/Animations/GradualBlur/GradualBlur.css @@ -0,0 +1,20 @@ +/* GradualBlur.css */ + +/* Container */ +.gradual-blur { + position: relative; + display: flex; + overflow: hidden; + pointer-events: none; /* so it doesn’t block interactions */ +} + +/* Common gradient layers */ +.gradual-blur > div { + position: absolute; + left: 0; + top: 0; + right: 0; + bottom: 0; + transition: opacity 0.3s ease; + will-change: opacity; +} diff --git a/src/ts-default/Animations/GradualBlur/GradualBlur.tsx b/src/ts-default/Animations/GradualBlur/GradualBlur.tsx new file mode 100644 index 00000000..a6846e0e --- /dev/null +++ b/src/ts-default/Animations/GradualBlur/GradualBlur.tsx @@ -0,0 +1,416 @@ +import React, { + CSSProperties, + useEffect, + useRef, + useState, + useMemo, + useCallback, + PropsWithChildren, +} from "react"; +import * as math from "mathjs"; +import "./Gradualblur.css"; + +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 DEFAULT_CONFIG: Partial = { + position: 'bottom', + strength: 2, + height: '6rem', + divCount: 5, + exponential: true, + zIndex: 40, + animated: false, + duration: '0.3s', + easing: 'ease-out', + opacity: 1, + curve: 'bezier', + responsive: false, + target: 'page', + className: '', + style: {} +}; + + +const PRESETS: Record> = { + + 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 number> = { + linear: (progress) => progress, + bezier: (progress) => progress * progress * (3 - 2 * progress), + 'ease-in': (progress) => progress * progress, + 'ease-out': (progress) => 1 - Math.pow(1 - progress, 2), + 'ease-in-out': (progress) => + progress < 0.5 ? 2 * progress * progress : 1 - Math.pow(-2 * progress + 2, 2) / 2 +}; + +const mergeConfigs = (...configs: Partial[]): Partial => { + return configs.reduce((acc, config) => ({ ...acc, ...config }), {}); +}; + +const getGradientDirection = (position: string): string => { + const directions: Record = { + top: 'to top', + bottom: 'to bottom', + left: 'to left', + right: 'to right' + }; + return directions[position] || 'to bottom'; +}; + +const debounce = void>(func: T, wait: number): (...args: Parameters) => void => { + let timeout: NodeJS.Timeout; + return function executedFunction(...args: Parameters) { + const later = () => { + clearTimeout(timeout); + func(...args); + }; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + }; +}; + + +const useResponsiveHeight = (responsive: boolean = false, config: Partial) => { + const [height, setHeight] = useState(config.height); + + const updateHeight = useCallback(debounce(() => { + if (!responsive) return; + + const screenWidth = window.innerWidth; + let newHeight = config.height; + + if (screenWidth <= 480 && config.mobileHeight) { + newHeight = config.mobileHeight; + } else if (screenWidth <= 768 && config.tabletHeight) { + newHeight = config.tabletHeight; + } else if (screenWidth <= 1024 && config.desktopHeight) { + newHeight = config.desktopHeight; + } + + setHeight(newHeight); + }, 100), [responsive, config]); + + useEffect(() => { + if (!responsive) return; + + updateHeight(); + window.addEventListener('resize', updateHeight); + return () => window.removeEventListener('resize', updateHeight); + }, [responsive, updateHeight]); + + return responsive ? height : config.height; +}; + +const useResponsiveWidth = (responsive: boolean = false, config: Partial) => { + const [width, setWidth] = useState(config.width); + + const updateWidth = useCallback(debounce(() => { + if (!responsive) return; + + const screenWidth = window.innerWidth; + let newWidth = config.width; + + if (screenWidth <= 480 && config.mobileWidth) { + newWidth = config.mobileWidth; + } else if (screenWidth <= 768 && config.tabletWidth) { + newWidth = config.tabletWidth; + } else if (screenWidth <= 1024 && config.desktopWidth) { + newWidth = config.desktopWidth; + } + + setWidth(newWidth); + }, 100), [responsive, config]); + + useEffect(() => { + if (!responsive) return; + + updateWidth(); + window.addEventListener('resize', updateWidth); + return () => window.removeEventListener('resize', updateWidth); + }, [responsive, updateWidth]); + + return responsive ? width : config.width; +}; + +const useIntersectionObserver = (ref: React.RefObject, shouldObserve: boolean = false) => { + const [isVisible, setIsVisible] = useState(!shouldObserve); + + useEffect(() => { + if (!shouldObserve || !ref.current) return; + + const observer = new IntersectionObserver( + ([entry]) => setIsVisible(entry.isIntersecting), + { threshold: 0.1 } + ); + + observer.observe(ref.current); + return () => observer.disconnect(); + }, [ref, shouldObserve]); + + return isVisible; +}; + +const GradualBlur: React.FC> = (props) => { + const containerRef = useRef(null); + const [isHovered, setIsHovered] = useState(false); + + + const config = useMemo(() => { + const presetConfig = props.preset && PRESETS[props.preset] ? PRESETS[props.preset] : {}; + return mergeConfigs(DEFAULT_CONFIG, presetConfig, props) as Required; + }, [props]); + + + const responsiveHeight = useResponsiveHeight(config.responsive, config); + const responsiveWidth = useResponsiveWidth(config.responsive, config); + + const isVisible = useIntersectionObserver(containerRef, config.animated === 'scroll'); + + + const blurDivs = useMemo(() => { + const divs: React.ReactNode[] = []; + const increment = 100 / config.divCount; + const currentStrength = isHovered && config.hoverIntensity ? + config.strength * config.hoverIntensity : config.strength; + + const curveFunc = CURVE_FUNCTIONS[config.curve] || CURVE_FUNCTIONS.linear; + + for (let i = 1; i <= config.divCount; i++) { + let progress = i / config.divCount; + progress = curveFunc(progress); + + // Calculate blur value + let blurValue: number; + if (config.exponential) { + blurValue = Number(math.pow(2, progress * 4)) * 0.0625 * currentStrength; + } else { + blurValue = 0.0625 * (progress * config.divCount + 1) * currentStrength; + } + + // Calculate gradient positions + 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; + + // Generate gradient + let gradient = `transparent ${p1}%, black ${p2}%`; + if (p3 <= 100) gradient += `, black ${p3}%`; + if (p4 <= 100) gradient += `, transparent ${p4}%`; + + const direction = getGradientDirection(config.position); + + const divStyle: CSSProperties = { + position: 'absolute', + inset: '0', + maskImage: `linear-gradient(${direction}, ${gradient})`, + WebkitMaskImage: `linear-gradient(${direction}, ${gradient})`, + backdropFilter: `blur(${blurValue.toFixed(3)}rem)`, + WebkitBackdropFilter: `blur(${blurValue.toFixed(3)}rem)`, + opacity: config.opacity, + transition: config.animated && config.animated !== 'scroll' ? + `backdrop-filter ${config.duration} ${config.easing}` : undefined + }; + + divs.push(
); + } + + return divs; + }, [config, isHovered]); + + const containerStyle: CSSProperties = useMemo(() => { + const isVertical = ['top', 'bottom'].includes(config.position); + const isHorizontal = ['left', 'right'].includes(config.position); + const isPageTarget = config.target === 'page'; + + const baseStyle: CSSProperties = { + + position: isPageTarget ? 'fixed' : 'absolute', + pointerEvents: config.hoverIntensity ? 'auto' : 'none', + opacity: isVisible ? 1 : 0, + transition: config.animated ? `opacity ${config.duration} ${config.easing}` : undefined, + zIndex: isPageTarget ? config.zIndex + 100 : config.zIndex, + ...config.style + }; + + + if (isVertical) { + baseStyle.height = responsiveHeight; + baseStyle.width = responsiveWidth || '100%'; + baseStyle[config.position] = 0; + baseStyle.left = 0; + baseStyle.right = 0; + } else if (isHorizontal) { + baseStyle.width = responsiveWidth || responsiveHeight; + baseStyle.height = '100%'; + baseStyle[config.position] = 0; + baseStyle.top = 0; + baseStyle.bottom = 0; + } + + return baseStyle; + }, [config, responsiveHeight, responsiveWidth, isVisible]); + + + const handleMouseEnter = useCallback(() => { + if (config.hoverIntensity) { + setIsHovered(true); + } + }, [config.hoverIntensity]); + + const handleMouseLeave = useCallback(() => { + if (config.hoverIntensity) { + setIsHovered(false); + } + }, [config.hoverIntensity]); + + + useEffect(() => { + if (isVisible && config.animated === 'scroll' && config.onAnimationComplete) { + const timer = setTimeout( + () => config.onAnimationComplete!(), + parseFloat(config.duration) * 1000 + ); + return () => clearTimeout(timer); + } + }, [isVisible, config.animated, config.duration, config.onAnimationComplete]); + + return ( +
+
+ {blurDivs} +
+
+ ); +}; + + +export const createPageBlur = (props: Partial = {}) => { + return ; +}; + +export const createParentBlur = (props: Partial = {}) => { + return ; +}; + + +export { PRESETS, CURVE_FUNCTIONS }; + +export default React.memo(GradualBlur); + + +const injectStyles = () => { + if (typeof document === 'undefined') return; + + const styleId = 'gradual-blur-styles'; + if (document.getElementById(styleId)) return; + + const styleElement = document.createElement('style'); + styleElement.id = styleId; + styleElement.textContent = ` + .gradual-blur { + pointer-events: none; + } + + .gradual-blur-page { + } + + .gradual-blur-parent { + } + + .gradual-blur-inner { + pointer-events: none; + } + + .gradual-blur:hover .gradual-blur-inner { + } + + .gradual-blur { + transition: opacity 0.3s ease-out; + } + + @media (max-width: 480px) { + .gradual-blur-responsive { + } + } + + @media (max-width: 768px) { + .gradual-blur-responsive { + } + } + `; + + document.head.appendChild(styleElement); +}; + +if (typeof document !== 'undefined') { + injectStyles(); +} diff --git a/src/ts-tailwind/Animations/GradualBlur/GradualBlur.tsx b/src/ts-tailwind/Animations/GradualBlur/GradualBlur.tsx new file mode 100644 index 00000000..0dd9676b --- /dev/null +++ b/src/ts-tailwind/Animations/GradualBlur/GradualBlur.tsx @@ -0,0 +1,425 @@ +import React, { + CSSProperties, + useEffect, + useRef, + useState, + useMemo, + useCallback, + PropsWithChildren, +} from "react"; +import * as math from "mathjs"; + +type GradualBlurProps = PropsWithChildren<{ + + 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 DEFAULT_CONFIG: Partial = { + position: 'bottom', + strength: 2, + height: '6rem', + divCount: 5, + exponential: true, + zIndex: 40, + animated: false, + duration: '0.3s', + easing: 'ease-out', + opacity: 1, + curve: 'bezier', + responsive: false, + target: 'page', + className: '', + style: {} +}; + + +const PRESETS: Record> = { + + 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 number> = { + linear: (progress) => progress, + bezier: (progress) => progress * progress * (3 - 2 * progress), + 'ease-in': (progress) => progress * progress, + 'ease-out': (progress) => 1 - Math.pow(1 - progress, 2), + 'ease-in-out': (progress) => + progress < 0.5 ? 2 * progress * progress : 1 - Math.pow(-2 * progress + 2, 2) / 2 +}; + + +const mergeConfigs = (...configs: Partial[]): Partial => { + return configs.reduce((acc, config) => ({ ...acc, ...config }), {}); +}; + +const getGradientDirection = (position: string): string => { + const directions: Record = { + top: 'to top', + bottom: 'to bottom', + left: 'to left', + right: 'to right' + }; + return directions[position] || 'to bottom'; +}; + +const debounce = void>(func: T, wait: number): (...args: Parameters) => void => { + let timeout: NodeJS.Timeout; + return function executedFunction(...args: Parameters) { + const later = () => { + clearTimeout(timeout); + func(...args); + }; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + }; +}; + + +const useResponsiveHeight = (responsive: boolean = false, config: Partial) => { + const [height, setHeight] = useState(config.height); + + const updateHeight = useCallback(debounce(() => { + if (!responsive) return; + + const screenWidth = window.innerWidth; + let newHeight = config.height; + + if (screenWidth <= 480 && config.mobileHeight) { + newHeight = config.mobileHeight; + } else if (screenWidth <= 768 && config.tabletHeight) { + newHeight = config.tabletHeight; + } else if (screenWidth <= 1024 && config.desktopHeight) { + newHeight = config.desktopHeight; + } + + setHeight(newHeight); + }, 100), [responsive, config]); + + useEffect(() => { + if (!responsive) return; + + updateHeight(); + window.addEventListener('resize', updateHeight); + return () => window.removeEventListener('resize', updateHeight); + }, [responsive, updateHeight]); + + return responsive ? height : config.height; +}; + +const useResponsiveWidth = (responsive: boolean = false, config: Partial) => { + const [width, setWidth] = useState(config.width); + + const updateWidth = useCallback(debounce(() => { + if (!responsive) return; + + const screenWidth = window.innerWidth; + let newWidth = config.width; + + if (screenWidth <= 480 && config.mobileWidth) { + newWidth = config.mobileWidth; + } else if (screenWidth <= 768 && config.tabletWidth) { + newWidth = config.tabletWidth; + } else if (screenWidth <= 1024 && config.desktopWidth) { + newWidth = config.desktopWidth; + } + + setWidth(newWidth); + }, 100), [responsive, config]); + + useEffect(() => { + if (!responsive) return; + + updateWidth(); + window.addEventListener('resize', updateWidth); + return () => window.removeEventListener('resize', updateWidth); + }, [responsive, updateWidth]); + + return responsive ? width : config.width; +}; + +const useIntersectionObserver = (ref: React.RefObject, shouldObserve: boolean = false) => { + const [isVisible, setIsVisible] = useState(!shouldObserve); + + useEffect(() => { + if (!shouldObserve || !ref.current) return; + + const observer = new IntersectionObserver( + ([entry]) => setIsVisible(entry.isIntersecting), + { threshold: 0.1 } + ); + + observer.observe(ref.current); + return () => observer.disconnect(); + }, [ref, shouldObserve]); + + return isVisible; +}; + +const GradualBlur: React.FC = (props) => { + const containerRef = useRef(null) as React.RefObject; + const [isHovered, setIsHovered] = useState(false); + + + const config = useMemo(() => { + const presetConfig = props.preset && PRESETS[props.preset] ? PRESETS[props.preset] : {}; + return mergeConfigs(DEFAULT_CONFIG, presetConfig, props) as Required; + }, [props]); + + + const responsiveHeight = useResponsiveHeight(config.responsive, config); + const responsiveWidth = useResponsiveWidth(config.responsive, config); + + + const isVisible = useIntersectionObserver(containerRef, config.animated === 'scroll'); + + + const blurDivs = useMemo(() => { + const divs: React.ReactNode[] = []; + const increment = 100 / config.divCount; + const currentStrength = isHovered && config.hoverIntensity ? + config.strength * config.hoverIntensity : config.strength; + + const curveFunc = CURVE_FUNCTIONS[config.curve] || CURVE_FUNCTIONS.linear; + + for (let i = 1; i <= config.divCount; i++) { + let progress = i / config.divCount; + progress = curveFunc(progress); + + + let blurValue: number; + if (config.exponential) { + blurValue = Number(math.pow(2, progress * 4)) * 0.0625 * currentStrength; + } else { + blurValue = 0.0625 * (progress * config.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.position); + + const divStyle: CSSProperties = { + maskImage: `linear-gradient(${direction}, ${gradient})`, + WebkitMaskImage: `linear-gradient(${direction}, ${gradient})`, + backdropFilter: `blur(${blurValue.toFixed(3)}rem)`, + opacity: config.opacity, + transition: config.animated && config.animated !== 'scroll' ? + `backdrop-filter ${config.duration} ${config.easing}` : undefined + }; + + divs.push( +
+ ); + } + + return divs; + }, [config, isHovered]); + + + const containerStyle: CSSProperties = useMemo(() => { + const isVertical = ['top', 'bottom'].includes(config.position); + const isHorizontal = ['left', 'right'].includes(config.position); + const isPageTarget = config.target === 'page'; + + const baseStyle: CSSProperties = { + + position: isPageTarget ? 'fixed' : 'absolute', + pointerEvents: config.hoverIntensity ? 'auto' : 'none', + opacity: isVisible ? 1 : 0, + transition: config.animated ? `opacity ${config.duration} ${config.easing}` : undefined, + zIndex: isPageTarget ? config.zIndex + 100 : config.zIndex, + ...config.style + }; + + + if (isVertical) { + baseStyle.height = responsiveHeight; + baseStyle.width = responsiveWidth || '100%'; + baseStyle[config.position] = 0; + baseStyle.left = 0; + baseStyle.right = 0; + } else if (isHorizontal) { + baseStyle.width = responsiveWidth || responsiveHeight; + baseStyle.height = '100%'; + baseStyle[config.position] = 0; + baseStyle.top = 0; + baseStyle.bottom = 0; + } + + return baseStyle; + }, [config, responsiveHeight, responsiveWidth, isVisible]); + + + const handleMouseEnter = useCallback(() => { + if (config.hoverIntensity) { + setIsHovered(true); + } + }, [config.hoverIntensity]); + + const handleMouseLeave = useCallback(() => { + if (config.hoverIntensity) { + setIsHovered(false); + } + }, [config.hoverIntensity]); + + + useEffect(() => { + if (isVisible && config.animated === 'scroll' && config.onAnimationComplete) { + const timer = setTimeout( + () => config.onAnimationComplete!(), + parseFloat(config.duration) * 1000 + ); + return () => clearTimeout(timer); + } + }, [isVisible, config.animated, config.duration, config.onAnimationComplete]); + + return ( +
+
{blurDivs}
+ {props.children &&
{props.children}
} +
+ ); +}; + + +export const createPageBlur = (props: Partial = {}) => { + return ; +}; + +export const createParentBlur = (props: Partial = {}) => { + return ; +}; + + +export { PRESETS, CURVE_FUNCTIONS }; + +export default React.memo(GradualBlur); + + +const injectStyles = () => { + if (typeof document === 'undefined') return; + + const styleId = 'gradual-blur-styles'; + if (document.getElementById(styleId)) return; + + const styleElement = document.createElement('style'); + styleElement.id = styleId; + styleElement.textContent = ` + .gradual-blur { + pointer-events: none; + } + + .gradual-blur-page { + /* Page-level blur styles */ + } + + .gradual-blur-parent { + /* Parent-level blur styles */ + } + + .gradual-blur-inner { + pointer-events: none; + } + + /* Hover support */ + .gradual-blur:hover .gradual-blur-inner { + /* Hover effects can be added here */ + } + + /* Animation support */ + .gradual-blur { + transition: opacity 0.3s ease-out; + } + + /* Responsive utilities */ + @media (max-width: 480px) { + .gradual-blur-responsive { + /* Mobile specific styles */ + } + } + + @media (max-width: 768px) { + .gradual-blur-responsive { + /* Tablet specific styles */ + } + } + `; + + document.head.appendChild(styleElement); +}; + +if (typeof document !== 'undefined') { + injectStyles(); +}