mirror of
https://github.com/DavidHDev/vue-bits.git
synced 2026-03-07 22:49:31 -07:00
Compare commits
19 Commits
54c1941143
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9566e27aa9 | ||
|
|
b137028604 | ||
|
|
2c07d94a57 | ||
|
|
446b5e0e26 | ||
|
|
ea8a95aafc | ||
|
|
932767e855 | ||
|
|
3f32d17706 | ||
|
|
4443ec3e33 | ||
|
|
7368a6cecb | ||
|
|
3b7da99d89 | ||
|
|
7d921d26ca | ||
|
|
509a31cca2 | ||
|
|
80c42e7af4 | ||
|
|
dbcaa6f488 | ||
|
|
7d1e9b0075 | ||
|
|
f95fef0de0 | ||
|
|
2e39338725 | ||
|
|
101dde4c6d | ||
|
|
d312302823 |
36
package-lock.json
generated
36
package-lock.json
generated
@@ -22,7 +22,7 @@
|
|||||||
"lucide-vue-next": "^0.548.0",
|
"lucide-vue-next": "^0.548.0",
|
||||||
"mathjs": "^14.6.0",
|
"mathjs": "^14.6.0",
|
||||||
"matter-js": "^0.20.0",
|
"matter-js": "^0.20.0",
|
||||||
"motion-v": "^1.5.0",
|
"motion-v": "^1.10.2",
|
||||||
"ogl": "^1.0.11",
|
"ogl": "^1.0.11",
|
||||||
"postprocessing": "^6.37.6",
|
"postprocessing": "^6.37.6",
|
||||||
"primeicons": "^7.0.0",
|
"primeicons": "^7.0.0",
|
||||||
@@ -8164,13 +8164,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/framer-motion": {
|
"node_modules/framer-motion": {
|
||||||
"version": "12.23.12",
|
"version": "12.29.2",
|
||||||
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.12.tgz",
|
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.29.2.tgz",
|
||||||
"integrity": "sha512-6e78rdVtnBvlEVgu6eFEAgG9v3wLnYEboM8I5O5EXvfKC8gxGQB8wXJdhkMy10iVcn05jl6CNw7/HTsTCfwcWg==",
|
"integrity": "sha512-lSNRzBJk4wuIy0emYQ/nfZ7eWhqud2umPKw2QAQki6uKhZPKm2hRQHeQoHTG9MIvfobb+A/LbEWPJU794ZUKrg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"motion-dom": "^12.23.12",
|
"motion-dom": "^12.29.2",
|
||||||
"motion-utils": "^12.23.6",
|
"motion-utils": "^12.29.2",
|
||||||
"tslib": "^2.4.0"
|
"tslib": "^2.4.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
@@ -9984,29 +9984,29 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/motion-dom": {
|
"node_modules/motion-dom": {
|
||||||
"version": "12.23.12",
|
"version": "12.29.2",
|
||||||
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.12.tgz",
|
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.29.2.tgz",
|
||||||
"integrity": "sha512-RcR4fvMCTESQBD/uKQe49D5RUeDOokkGRmz4ceaJKDBgHYtZtntC/s2vLvY38gqGaytinij/yi3hMcWVcEF5Kw==",
|
"integrity": "sha512-/k+NuycVV8pykxyiTCoFzIVLA95Nb1BFIVvfSu9L50/6K6qNeAYtkxXILy/LRutt7AzaYDc2myj0wkCVVYAPPA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"motion-utils": "^12.23.6"
|
"motion-utils": "^12.29.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/motion-utils": {
|
"node_modules/motion-utils": {
|
||||||
"version": "12.23.6",
|
"version": "12.29.2",
|
||||||
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.23.6.tgz",
|
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.29.2.tgz",
|
||||||
"integrity": "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==",
|
"integrity": "sha512-G3kc34H2cX2gI63RqU+cZq+zWRRPSsNIOjpdl9TN4AQwC4sgwYPl/Q/Obf/d53nOm569T0fYK+tcoSV50BWx8A==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/motion-v": {
|
"node_modules/motion-v": {
|
||||||
"version": "1.7.1",
|
"version": "1.10.2",
|
||||||
"resolved": "https://registry.npmjs.org/motion-v/-/motion-v-1.7.1.tgz",
|
"resolved": "https://registry.npmjs.org/motion-v/-/motion-v-1.10.2.tgz",
|
||||||
"integrity": "sha512-B22fYcHGx05moUtoIH0ZP/JzeacGOHzLkLmMTKU9tRB+uVMSfgqiXVzZb602qiG1ap8W7TZ+5RD5R3MmODu9oA==",
|
"integrity": "sha512-K+Zus21KKgZP4CBY7CvU/B7UZCV9sZTHG0FgsAfGHlbZi+u8EolmZ2kvJe5zOG0RzCgdiVCobHBt54qch9rweg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"framer-motion": "12.23.12",
|
"framer-motion": "^12.25.0",
|
||||||
"hey-listen": "^1.0.8",
|
"hey-listen": "^1.0.8",
|
||||||
"motion-dom": "12.23.12"
|
"motion-dom": "^12.23.23"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@vueuse/core": ">=10.0.0",
|
"@vueuse/core": ">=10.0.0",
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
"lucide-vue-next": "^0.548.0",
|
"lucide-vue-next": "^0.548.0",
|
||||||
"mathjs": "^14.6.0",
|
"mathjs": "^14.6.0",
|
||||||
"matter-js": "^0.20.0",
|
"matter-js": "^0.20.0",
|
||||||
"motion-v": "^1.5.0",
|
"motion-v": "^1.10.2",
|
||||||
"ogl": "^1.0.11",
|
"ogl": "^1.0.11",
|
||||||
"postprocessing": "^6.37.6",
|
"postprocessing": "^6.37.6",
|
||||||
"primeicons": "^7.0.0",
|
"primeicons": "^7.0.0",
|
||||||
|
|||||||
BIN
public/assets/videos/grainient.mp4
Normal file
BIN
public/assets/videos/grainient.mp4
Normal file
Binary file not shown.
BIN
public/assets/videos/grainient.webm
Normal file
BIN
public/assets/videos/grainient.webm
Normal file
Binary file not shown.
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
|||||||
{"name":"CircularText","title":"CircularText","description":"Layouts characters around a circle with optional rotation animation.","type":"registry:component","add":"when-added","files":[{"type":"registry:component","role":"file","content":"<script setup lang=\"ts\">\nimport { computed, ref, watchEffect, onUnmounted } from 'vue';\nimport { Motion } from 'motion-v';\n\ninterface CircularTextProps {\n text: string;\n spinDuration?: number;\n onHover?: 'slowDown' | 'speedUp' | 'pause' | 'goBonkers';\n className?: string;\n}\n\nconst props = withDefaults(defineProps<CircularTextProps>(), {\n text: '',\n spinDuration: 20,\n onHover: 'speedUp',\n className: ''\n});\n\nconst letters = computed(() => Array.from(props.text));\nconst isHovered = ref(false);\n\nconst currentRotation = ref(0);\nconst animationId = ref<number | null>(null);\nconst lastTime = ref<number>(Date.now());\nconst rotationSpeed = ref<number>(0);\n\nconst getCurrentSpeed = () => {\n if (isHovered.value && props.onHover === 'pause') return 0;\n\n const baseDuration = props.spinDuration;\n const baseSpeed = 360 / baseDuration;\n\n if (!isHovered.value) return baseSpeed;\n\n switch (props.onHover) {\n case 'slowDown':\n return baseSpeed / 2;\n case 'speedUp':\n return baseSpeed * 4;\n case 'goBonkers':\n return baseSpeed * 20;\n default:\n return baseSpeed;\n }\n};\n\nconst getCurrentScale = () => {\n return isHovered.value && props.onHover === 'goBonkers' ? 0.8 : 1;\n};\n\nconst animate = () => {\n const now = Date.now();\n const deltaTime = (now - lastTime.value) / 1000;\n lastTime.value = now;\n\n const targetSpeed = getCurrentSpeed();\n\n const speedDiff = targetSpeed - rotationSpeed.value;\n const smoothingFactor = Math.min(1, deltaTime * 5);\n rotationSpeed.value += speedDiff * smoothingFactor;\n\n currentRotation.value = (currentRotation.value + rotationSpeed.value * deltaTime) % 360;\n\n animationId.value = requestAnimationFrame(animate);\n};\n\nconst startAnimation = () => {\n if (animationId.value) {\n cancelAnimationFrame(animationId.value);\n }\n lastTime.value = Date.now();\n rotationSpeed.value = getCurrentSpeed();\n animate();\n};\n\nwatchEffect(() => {\n startAnimation();\n});\n\nstartAnimation();\n\nonUnmounted(() => {\n if (animationId.value) {\n cancelAnimationFrame(animationId.value);\n }\n});\n\nconst handleHoverStart = () => {\n isHovered.value = true;\n};\n\nconst handleHoverEnd = () => {\n isHovered.value = false;\n};\n\nconst getLetterTransform = (index: number) => {\n const rotationDeg = (360 / letters.value.length) * index;\n const factor = Math.PI / letters.value.length;\n const x = factor * index;\n const y = factor * index;\n return `rotateZ(${rotationDeg}deg) translate3d(${x}px, ${y}px, 0)`;\n};\n</script>\n\n<template>\n <Motion\n :animate=\"{\n rotate: currentRotation,\n scale: getCurrentScale()\n }\"\n :transition=\"{\n rotate: {\n duration: 0\n },\n scale: {\n type: 'spring',\n damping: 20,\n stiffness: 300\n }\n }\"\n :class=\"`m-0 mx-auto rounded-full w-[200px] h-[200px] relative font-black text-white text-center cursor-pointer origin-center ${props.className}`\"\n @mouseenter=\"handleHoverStart\"\n @mouseleave=\"handleHoverEnd\"\n >\n <span\n v-for=\"(letter, i) in letters\"\n :key=\"i\"\n class=\"absolute inline-block inset-0 text-2xl transition-all duration-500 ease-[cubic-bezier(0,0,0,1)]\"\n :style=\"{\n transform: getLetterTransform(i),\n WebkitTransform: getLetterTransform(i)\n }\"\n >\n {{ letter }}\n </span>\n </Motion>\n</template>\n","path":"CircularText/CircularText.vue","_imports_":[],"registryDependencies":[],"dependencies":[],"devDependencies":[]}],"registryDependencies":[],"dependencies":[{"ecosystem":"js","name":"motion-v","version":"^1.5.0"}],"devDependencies":[],"categories":["TextAnimations"]}
|
{"name":"CircularText","title":"CircularText","description":"Layouts characters around a circle with optional rotation animation.","type":"registry:component","add":"when-added","files":[{"type":"registry:component","role":"file","content":"<script setup lang=\"ts\">\nimport { animate, Motion, MotionValue, useMotionValue } from 'motion-v';\nimport { computed, onMounted, watch } from 'vue';\n\ninterface CircularTextProps {\n text: string;\n spinDuration?: number;\n onHover?: 'slowDown' | 'speedUp' | 'pause' | 'goBonkers';\n className?: string;\n}\n\nconst props = withDefaults(defineProps<CircularTextProps>(), {\n spinDuration: 20,\n onHover: 'speedUp',\n className: ''\n});\n\nconst letters = computed(() => Array.from(props.text));\nconst rotation: MotionValue<number> = useMotionValue(0);\n\nlet currentAnimation: ReturnType<typeof animate> | null = null;\n\nconst startRotation = (duration: number) => {\n currentAnimation?.stop();\n const start = rotation.get();\n\n currentAnimation = animate(rotation, start + 360, {\n duration,\n ease: 'linear',\n repeat: Infinity\n });\n};\n\nonMounted(() => {\n startRotation(props.spinDuration);\n});\n\nwatch(\n () => [props.spinDuration, props.text],\n () => {\n startRotation(props.spinDuration);\n }\n);\n\nconst handleHoverStart = () => {\n if (!props.onHover) return;\n\n switch (props.onHover) {\n case 'slowDown':\n startRotation(props.spinDuration * 2);\n break;\n case 'speedUp':\n startRotation(props.spinDuration / 4);\n break;\n case 'pause':\n currentAnimation?.stop();\n break;\n case 'goBonkers':\n startRotation(props.spinDuration / 20);\n break;\n }\n};\n\nconst handleHoverEnd = () => {\n startRotation(props.spinDuration);\n};\n\nconst getLetterTransform = (index: number) => {\n const rotationDeg = (360 / letters.value.length) * index;\n const factor = Math.PI / letters.value.length;\n const x = factor * index;\n const y = factor * index;\n return `rotateZ(${rotationDeg}deg) translate3d(${x}px, ${y}px, 0)`;\n};\n</script>\n\n<template>\n <Motion\n tag=\"div\"\n :class=\"[\n 'm-0 mx-auto rounded-full w-[200px] h-[200px] relative font-black text-white text-center cursor-pointer origin-center',\n className\n ]\"\n :style=\"{\n rotate: rotation\n }\"\n :initial=\"{\n rotate: 0\n }\"\n @mouseenter=\"handleHoverStart\"\n @mouseleave=\"handleHoverEnd\"\n >\n <span\n v-for=\"(letter, i) in letters\"\n :key=\"i\"\n class=\"inline-block absolute inset-0 text-2xl transition-all duration-500 ease-[cubic-bezier(0,0,0,1)]\"\n :style=\"{\n transform: getLetterTransform(i),\n WebkitTransform: getLetterTransform(i)\n }\"\n >\n {{ letter }}\n </span>\n </Motion>\n</template>\n","path":"CircularText/CircularText.vue","_imports_":[],"registryDependencies":[],"dependencies":[],"devDependencies":[]}],"registryDependencies":[],"dependencies":[{"ecosystem":"js","name":"motion-v","version":"^1.10.2"}],"devDependencies":[],"categories":["TextAnimations"]}
|
||||||
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
|||||||
{"name":"GradientText","title":"GradientText","description":"Animated gradient sweep across live text with speed and color control.","type":"registry:component","add":"when-added","files":[{"type":"registry:component","role":"file","content":"<script setup lang=\"ts\">\nimport { computed } from 'vue';\n\ninterface GradientTextProps {\n text: string;\n className?: string;\n colors?: string[];\n animationSpeed?: number;\n showBorder?: boolean;\n}\n\nconst props = withDefaults(defineProps<GradientTextProps>(), {\n text: '',\n className: '',\n colors: () => ['#ffaa40', '#9c40ff', '#ffaa40'],\n animationSpeed: 8,\n showBorder: false\n});\n\nconst gradientStyle = computed(() => ({\n backgroundImage: `linear-gradient(to right, ${props.colors.join(', ')})`,\n animationDuration: `${props.animationSpeed}s`,\n backgroundSize: '300% 100%',\n '--animation-duration': `${props.animationSpeed}s`\n}));\n\nconst borderStyle = computed(() => ({\n ...gradientStyle.value\n}));\n\nconst textStyle = computed(() => ({\n ...gradientStyle.value,\n backgroundClip: 'text',\n WebkitBackgroundClip: 'text'\n}));\n</script>\n\n<template>\n <div\n :class=\"`relative mx-auto flex max-w-fit flex-row items-center justify-center rounded-[1.25rem] font-medium backdrop-blur transition-shadow duration-500 overflow-hidden cursor-pointer ${className}`\"\n >\n <div\n v-if=\"showBorder\"\n class=\"absolute inset-0 bg-cover z-0 pointer-events-none animate-gradient\"\n :style=\"borderStyle\"\n >\n <div\n class=\"absolute inset-0 bg-black rounded-[1.25rem] z-[-1]\"\n style=\"width: calc(100% - 2px); height: calc(100% - 2px); left: 50%; top: 50%; transform: translate(-50%, -50%)\"\n />\n </div>\n\n <div class=\"inline-block relative z-2 text-transparent bg-cover animate-gradient\" :style=\"textStyle\">\n {{ text }}\n </div>\n </div>\n</template>\n\n<style scoped>\n@keyframes gradient {\n 0% {\n background-position: 0% 50%;\n }\n 50% {\n background-position: 100% 50%;\n }\n 100% {\n background-position: 0% 50%;\n }\n}\n\n.animate-gradient {\n animation: gradient var(--animation-duration, 8s) linear infinite;\n}\n</style>\n","path":"GradientText/GradientText.vue","_imports_":[],"registryDependencies":[],"dependencies":[],"devDependencies":[]}],"registryDependencies":[],"dependencies":[],"devDependencies":[],"categories":["TextAnimations"]}
|
{"name":"GradientText","title":"GradientText","description":"Animated gradient sweep across live text with speed and color control.","type":"registry:component","add":"when-added","files":[{"type":"registry:component","role":"file","content":"<script setup lang=\"ts\">\nimport { Motion, useAnimationFrame, useMotionValue, useTransform } from 'motion-v';\nimport { computed, ref, useSlots } from 'vue';\n\ninterface GradientTextProps {\n className?: string;\n colors?: string[];\n animationSpeed?: number;\n showBorder?: boolean;\n direction?: 'horizontal' | 'vertical' | 'diagonal';\n pauseOnHover?: boolean;\n yoyo?: boolean;\n}\n\nconst props = withDefaults(defineProps<GradientTextProps>(), {\n className: '',\n colors: () => ['#27FF64', '#27FF64', '#A0FFBC'],\n animationSpeed: 8,\n showBorder: false,\n direction: 'horizontal',\n pauseOnHover: false,\n yoyo: true\n});\n\nconst slots = useSlots();\nconst text = computed(() => (slots.default?.() ?? []).map(v => v.children).join(''));\n\nconst isPaused = ref(false);\nconst progress = useMotionValue(0);\nconst elapsedRef = ref(0);\nconst lastTimeRef = ref<number | null>(null);\n\nconst animationDuration = props.animationSpeed * 1000;\n\nuseAnimationFrame(time => {\n if (isPaused.value) {\n lastTimeRef.value = null;\n return;\n }\n\n if (lastTimeRef.value === null) {\n lastTimeRef.value = time;\n return;\n }\n\n const deltaTime = time - lastTimeRef.value;\n lastTimeRef.value = time;\n elapsedRef.value += deltaTime;\n\n if (props.yoyo) {\n const fullCycle = animationDuration * 2;\n const cycleTime = elapsedRef.value % fullCycle;\n\n if (cycleTime < animationDuration) {\n progress.set((cycleTime / animationDuration) * 100);\n } else {\n progress.set(100 - ((cycleTime - animationDuration) / animationDuration) * 100);\n }\n } else {\n // Continuously increase position for seamless looping\n progress.set((elapsedRef.value / animationDuration) * 100);\n }\n});\n\nconst backgroundPosition = useTransform(progress, p => {\n if (props.direction === 'horizontal') {\n return `${p}% 50%`;\n } else if (props.direction === 'vertical') {\n return `50% ${p}%`;\n } else {\n // For diagonal, move only horizontally to avoid interference patterns\n return `${p}% 50%`;\n }\n});\n\nconst handleMouseEnter = () => {\n if (props.pauseOnHover) isPaused.value = true;\n};\n\nconst handleMouseLeave = () => {\n if (props.pauseOnHover) isPaused.value = false;\n};\n\nconst gradientAngle = computed(() =>\n props.direction === 'horizontal' ? 'to right' : props.direction === 'vertical' ? 'to bottom' : 'to bottom right'\n);\n\n// Duplicate first color at the end for seamless looping\nconst gradientColors = computed(() => [...props.colors, props.colors[0]].join(', '));\n\nconst gradientStyle = computed(() => ({\n backgroundImage: `linear-gradient(${gradientAngle.value}, ${gradientColors.value})`,\n backgroundSize:\n props.direction === 'horizontal' ? '300% 100%' : props.direction === 'vertical' ? '100% 300%' : '300% 300%',\n backgroundRepeat: 'repeat'\n}));\n</script>\n\n<template>\n <Motion\n tag=\"div\"\n :class=\"[\n 'relative mx-auto flex max-w-fit flex-row items-center justify-center rounded-[1.25rem] font-medium backdrop-blur transition-shadow duration-500 overflow-hidden cursor-pointer',\n className,\n showBorder && 'py-1 px-2'\n ]\"\n @mouseenter=\"handleMouseEnter\"\n @mouseleave=\"handleMouseLeave\"\n >\n <Motion\n tag=\"div\"\n v-if=\"showBorder\"\n class=\"z-0 absolute inset-0 rounded-[1.25rem] pointer-events-none\"\n :style=\"{ ...gradientStyle, backgroundPosition }\"\n >\n <div\n class=\"z-[-1] absolute bg-black rounded-[1.25rem]\"\n :style=\"{\n width: 'calc(100% - 2px)',\n height: 'calc(100% - 2px)',\n left: '50%',\n top: '50%',\n transform: 'translate(-50%, -50%)'\n }\"\n />\n </Motion>\n\n <Motion\n tag=\"div\"\n class=\"inline-block z-2 relative bg-clip-text text-transparent\"\n :style=\"{\n ...gradientStyle,\n backgroundPosition,\n WebkitBackgroundClip: 'text'\n }\"\n >\n {{ text }}\n </Motion>\n </Motion>\n</template>\n","path":"GradientText/GradientText.vue","_imports_":[],"registryDependencies":[],"dependencies":[],"devDependencies":[]}],"registryDependencies":[],"dependencies":[{"ecosystem":"js","name":"motion-v","version":"^1.10.2"}],"devDependencies":[],"categories":["TextAnimations"]}
|
||||||
1
public/r/Grainient.json
Normal file
1
public/r/Grainient.json
Normal file
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
|||||||
{"name":"ShinyText","title":"ShinyText","description":"Metallic sheen sweeps across text producing a reflective highlight.","type":"registry:component","add":"when-added","files":[{"type":"registry:component","role":"file","content":"<script setup lang=\"ts\">\nimport { computed } from 'vue';\n\ninterface ShinyTextProps {\n text: string;\n disabled?: boolean;\n speed?: number;\n className?: string;\n}\n\nconst props = withDefaults(defineProps<ShinyTextProps>(), {\n text: '',\n disabled: false,\n speed: 5,\n className: ''\n});\n\nconst animationDuration = computed(() => `${props.speed}s`);\n</script>\n\n<template>\n <div\n :class=\"`text-[#b5b5b5a4] bg-clip-text inline-block ${!props.disabled ? 'animate-shine' : ''} ${props.className}`\"\n :style=\"{\n backgroundImage:\n 'linear-gradient(120deg, rgba(255, 255, 255, 0) 40%, rgba(255, 255, 255, 0.8) 50%, rgba(255, 255, 255, 0) 60%)',\n backgroundSize: '200% 100%',\n WebkitBackgroundClip: 'text',\n animationDuration: animationDuration\n }\"\n >\n {{ props.text }}\n </div>\n</template>\n\n<style scoped>\n@keyframes shine {\n 0% {\n background-position: 100%;\n }\n 100% {\n background-position: -100%;\n }\n}\n\n.animate-shine {\n animation: shine 5s linear infinite;\n}\n</style>\n","path":"ShinyText/ShinyText.vue","_imports_":[],"registryDependencies":[],"dependencies":[],"devDependencies":[]}],"registryDependencies":[],"dependencies":[],"devDependencies":[],"categories":["TextAnimations"]}
|
{"name":"ShinyText","title":"ShinyText","description":"Metallic sheen sweeps across text producing a reflective highlight.","type":"registry:component","add":"when-added","files":[{"type":"registry:component","role":"file","content":"<script setup lang=\"ts\">\nimport { Motion, useAnimationFrame, useMotionValue, useTransform } from 'motion-v';\nimport { computed, ref, watch } from 'vue';\n\ninterface ShinyTextProps {\n text: string;\n disabled?: boolean;\n speed?: number;\n className?: string;\n color?: string;\n shineColor?: string;\n spread?: number;\n yoyo?: boolean;\n pauseOnHover?: boolean;\n direction?: 'left' | 'right';\n delay?: number;\n}\n\nconst props = withDefaults(defineProps<ShinyTextProps>(), {\n disabled: false,\n speed: 2,\n className: '',\n color: '#b5b5b5',\n shineColor: '#ffffff',\n spread: 120,\n yoyo: false,\n pauseOnHover: false,\n direction: 'left',\n delay: 0\n});\n\nconst isPaused = ref(false);\nconst progress = useMotionValue(0);\nconst elapsedRef = ref(0);\nconst lastTimeRef = ref<number | null>(null);\nconst directionRef = ref(props.direction === 'left' ? 1 : -1);\n\nconst animationDuration = computed(() => props.speed * 1000);\nconst delayDuration = computed(() => props.delay * 1000);\n\nuseAnimationFrame(time => {\n if (props.disabled || isPaused.value) {\n lastTimeRef.value = null;\n return;\n }\n\n if (lastTimeRef.value === null) {\n lastTimeRef.value = time;\n return;\n }\n\n const deltaTime = time - lastTimeRef.value;\n lastTimeRef.value = time;\n\n elapsedRef.value += deltaTime;\n\n // Animation goes from 0 to 100\n if (props.yoyo) {\n const cycleDuration = animationDuration.value + delayDuration.value;\n const fullCycle = cycleDuration * 2;\n const cycleTime = elapsedRef.value % fullCycle;\n\n if (cycleTime < animationDuration.value) {\n // Forward animation: 0 -> 100\n const p = (cycleTime / animationDuration.value) * 100;\n progress.set(directionRef.value === 1 ? p : 100 - p);\n } else if (cycleTime < cycleDuration) {\n // Delay at end\n progress.set(directionRef.value === 1 ? 100 : 0);\n } else if (cycleTime < cycleDuration + animationDuration.value) {\n // Reverse animation: 100 -> 0\n const reverseTime = cycleTime - cycleDuration;\n const p = 100 - (reverseTime / animationDuration.value) * 100;\n progress.set(directionRef.value === 1 ? p : 100 - p);\n } else {\n // Delay at start\n progress.set(directionRef.value === 1 ? 0 : 100);\n }\n } else {\n const cycleDuration = animationDuration.value + delayDuration.value;\n const cycleTime = elapsedRef.value % cycleDuration;\n\n if (cycleTime < animationDuration.value) {\n // Animation phase: 0 -> 100\n const p = (cycleTime / animationDuration.value) * 100;\n progress.set(directionRef.value === 1 ? p : 100 - p);\n } else {\n // Delay phase - hold at end (shine off-screen)\n progress.set(directionRef.value === 1 ? 100 : 0);\n }\n }\n});\n\nwatch(\n () => props.direction,\n () => {\n directionRef.value = props.direction === 'left' ? 1 : -1;\n elapsedRef.value = 0;\n progress.set(0);\n },\n {\n immediate: true\n }\n);\n\nconst backgroundPosition = useTransform(progress, p => `${150 - p * 2}% center`);\n\nconst handleMouseEnter = () => {\n if (props.pauseOnHover) isPaused.value = true;\n};\n\nconst handleMouseLeave = () => {\n if (props.pauseOnHover) isPaused.value = false;\n};\n\nconst gradientStyle = computed(() => ({\n backgroundImage: `linear-gradient(${props.spread}deg, ${props.color} 0%, ${props.color} 35%, ${props.shineColor} 50%, ${props.color} 65%, ${props.color} 100%)`,\n backgroundSize: '200% auto',\n WebkitBackgroundClip: 'text',\n backgroundClip: 'text',\n WebkitTextFillColor: 'transparent'\n}));\n</script>\n\n<template>\n <Motion\n tag=\"span\"\n :class=\"['inline-block', className]\"\n :style=\"{ ...gradientStyle, backgroundPosition }\"\n @mouseenter=\"handleMouseEnter\"\n @mouseleave=\"handleMouseLeave\"\n >\n {{ text }}\n </Motion>\n</template>\n","path":"ShinyText/ShinyText.vue","_imports_":[],"registryDependencies":[],"dependencies":[],"devDependencies":[]}],"registryDependencies":[],"dependencies":[{"ecosystem":"js","name":"motion-v","version":"^1.10.2"}],"devDependencies":[],"categories":["TextAnimations"]}
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -19,10 +19,10 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="hero-main-content">
|
<div class="hero-main-content">
|
||||||
<router-link to="/backgrounds/pixel-snow" class="hero-new-badge-container">
|
<router-link to="/backgrounds/grainient" class="hero-new-badge-container">
|
||||||
<span class="hero-new-badge">Christmas Special 🎁</span>
|
<span class="hero-new-badge">New 🎉</span>
|
||||||
<div class="hero-new-badge-text">
|
<div class="hero-new-badge-text">
|
||||||
<span>Pixel Snow</span>
|
<span>Grainient</span>
|
||||||
<i class="pi-arrow-right pi" style="font-size: 0.8rem"></i>
|
<i class="pi-arrow-right pi" style="font-size: 0.8rem"></i>
|
||||||
</div>
|
</div>
|
||||||
</router-link>
|
</router-link>
|
||||||
@@ -83,7 +83,7 @@ const ResponsiveSplitText = defineComponent({
|
|||||||
isMobile: { type: Boolean, required: true },
|
isMobile: { type: Boolean, required: true },
|
||||||
text: { type: String, required: true },
|
text: { type: String, required: true },
|
||||||
className: { type: String, default: '' },
|
className: { type: String, default: '' },
|
||||||
splitType: { type: String as () => 'chars' | 'words' | 'lines' | 'words, chars', default: 'chars' },
|
splitType: { type: String as () => 'chars' | 'words' | 'lines', default: 'chars' },
|
||||||
delay: { type: Number, default: 100 },
|
delay: { type: Number, default: 100 },
|
||||||
duration: { type: Number, default: 0.6 },
|
duration: { type: Number, default: 0.6 },
|
||||||
ease: { type: String, default: 'power3.out' },
|
ease: { type: String, default: 'power3.out' },
|
||||||
|
|||||||
@@ -1,15 +1,12 @@
|
|||||||
// Highlighted sidebar items
|
// Highlighted sidebar items
|
||||||
export const NEW = [
|
export const NEW = [
|
||||||
'Antigravity',
|
|
||||||
'Color Bends',
|
'Color Bends',
|
||||||
'Floating Lines',
|
'Floating Lines',
|
||||||
'Ghost Cursor',
|
|
||||||
'Grid Scan',
|
|
||||||
'Laser Flow',
|
|
||||||
'Light Pillar',
|
'Light Pillar',
|
||||||
'Liquid Ether',
|
'Antigravity',
|
||||||
'Pixel Blast',
|
'Reflective Card',
|
||||||
'Pixel Snow',
|
'Pixel Snow',
|
||||||
|
'Grainient',
|
||||||
];
|
];
|
||||||
export const UPDATED = ['Metallic Paint'];
|
export const UPDATED = ['Metallic Paint'];
|
||||||
|
|
||||||
@@ -78,6 +75,7 @@ export const CATEGORIES = [
|
|||||||
'Star Border',
|
'Star Border',
|
||||||
'Sticker Peel',
|
'Sticker Peel',
|
||||||
'Target Cursor',
|
'Target Cursor',
|
||||||
|
'Orbit Images',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -133,6 +131,7 @@ export const CATEGORIES = [
|
|||||||
'Floating Lines',
|
'Floating Lines',
|
||||||
'Galaxy',
|
'Galaxy',
|
||||||
'Gradient Blinds',
|
'Gradient Blinds',
|
||||||
|
'Grainient',
|
||||||
'Grid Distortion',
|
'Grid Distortion',
|
||||||
'Grid Motion',
|
'Grid Motion',
|
||||||
'Grid Scan',
|
'Grid Scan',
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ const animations = {
|
|||||||
'ghost-cursor': () => import('../demo/Animations/GhostCursorDemo.vue'),
|
'ghost-cursor': () => import('../demo/Animations/GhostCursorDemo.vue'),
|
||||||
'antigravity': () => import('../demo/Animations/AntigravityDemo.vue'),
|
'antigravity': () => import('../demo/Animations/AntigravityDemo.vue'),
|
||||||
'pixel-trail': () => import('../demo/Animations/PixelTrailDemo.vue'),
|
'pixel-trail': () => import('../demo/Animations/PixelTrailDemo.vue'),
|
||||||
|
'orbit-images': () => import('../demo/Animations/OrbitImagesDemo.vue'),
|
||||||
};
|
};
|
||||||
|
|
||||||
const textAnimations = {
|
const textAnimations = {
|
||||||
@@ -127,6 +128,7 @@ const backgrounds = {
|
|||||||
'light-pillar': () => import('../demo/Backgrounds/LightPillarDemo.vue'),
|
'light-pillar': () => import('../demo/Backgrounds/LightPillarDemo.vue'),
|
||||||
'pixel-snow': () => import('../demo/Backgrounds/PixelSnowDemo.vue'),
|
'pixel-snow': () => import('../demo/Backgrounds/PixelSnowDemo.vue'),
|
||||||
'grid-scan': () => import('../demo/Backgrounds/GridScanDemo.vue'),
|
'grid-scan': () => import('../demo/Backgrounds/GridScanDemo.vue'),
|
||||||
|
'grainient': () => import('../demo/Backgrounds/GrainientDemo.vue'),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const componentMap = {
|
export const componentMap = {
|
||||||
|
|||||||
@@ -765,6 +765,14 @@ export const componentMetadata: ComponentMetadata = {
|
|||||||
docsUrl: 'https://vue-bits.dev/backgrounds/gradient-blinds',
|
docsUrl: 'https://vue-bits.dev/backgrounds/gradient-blinds',
|
||||||
tags: []
|
tags: []
|
||||||
},
|
},
|
||||||
|
'Backgrounds/Grainient': {
|
||||||
|
videoUrl: '/assets/videos/grainient.webm',
|
||||||
|
description: 'Grainy gradient swirls with soft wave distortion.',
|
||||||
|
category: 'Backgrounds',
|
||||||
|
name: 'Grainient',
|
||||||
|
docsUrl: 'https://vue-bits.dev/backgrounds/grainient',
|
||||||
|
tags: []
|
||||||
|
},
|
||||||
'Backgrounds/GridDistortion': {
|
'Backgrounds/GridDistortion': {
|
||||||
videoUrl: '/assets/videos/griddistortion.webm',
|
videoUrl: '/assets/videos/griddistortion.webm',
|
||||||
description: 'Warped grid mesh distorts smoothly reacting to cursor.',
|
description: 'Warped grid mesh distorts smoothly reacting to cursor.',
|
||||||
|
|||||||
30
src/constants/code/Animations/orbitImagesCode.ts
Normal file
30
src/constants/code/Animations/orbitImagesCode.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import code from '@/content/Animations/OrbitImages/OrbitImages.vue?raw';
|
||||||
|
import { createCodeObject } from '@/types/code';
|
||||||
|
|
||||||
|
export const orbitImages = createCodeObject(code, 'Animations/OrbitImages', {
|
||||||
|
usage: `<template>
|
||||||
|
<OrbitImages
|
||||||
|
:images="images"
|
||||||
|
shape="ellipse"
|
||||||
|
:radius-x="340"
|
||||||
|
:radius-y="80"
|
||||||
|
:rotation="-8"
|
||||||
|
:duration="30"
|
||||||
|
:item-size="80"
|
||||||
|
:responsive="true"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import OrbitImages from './OrbitImages.vue';
|
||||||
|
|
||||||
|
const images = [
|
||||||
|
'https://picsum.photos/300/300?grayscale&random=1',
|
||||||
|
'https://picsum.photos/300/300?grayscale&random=2',
|
||||||
|
'https://picsum.photos/300/300?grayscale&random=3',
|
||||||
|
'https://picsum.photos/300/300?grayscale&random=4',
|
||||||
|
'https://picsum.photos/300/300?grayscale&random=5',
|
||||||
|
'https://picsum.photos/300/300?grayscale&random=6'
|
||||||
|
];
|
||||||
|
</script>`
|
||||||
|
});
|
||||||
38
src/constants/code/Backgrounds/grainientCode.ts
Normal file
38
src/constants/code/Backgrounds/grainientCode.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import code from '@content/Backgrounds/Grainient/Grainient.vue?raw';
|
||||||
|
import { createCodeObject } from '../../../types/code';
|
||||||
|
|
||||||
|
export const grainient = createCodeObject(code, 'Backgrounds/Grainient', {
|
||||||
|
installation: `npm install ogl`,
|
||||||
|
usage: `<template>
|
||||||
|
<div style="width: 100%; height: 600px; position: relative;">
|
||||||
|
<Grainient
|
||||||
|
color1="#5227FF"
|
||||||
|
color2="#FF9FFC"
|
||||||
|
color3="#B19EEF"
|
||||||
|
:time-speed="0.25"
|
||||||
|
:color-balance="0.0"
|
||||||
|
:warp-strength="1.0"
|
||||||
|
:warp-frequency="5.0"
|
||||||
|
:warp-speed="2.0"
|
||||||
|
:warp-amplitude="50.0"
|
||||||
|
:blend-angle="0.0"
|
||||||
|
:blend-softness="0.05"
|
||||||
|
:rotation-amount="500.0"
|
||||||
|
:noise-scale="2.0"
|
||||||
|
:grain-amount="0.1"
|
||||||
|
:grain-scale="2.0"
|
||||||
|
:grain-animated="false"
|
||||||
|
:contrast="1.5"
|
||||||
|
:gamma="1.0"
|
||||||
|
:saturation="1.0"
|
||||||
|
:center-x="0.0"
|
||||||
|
:center-y="0.0"
|
||||||
|
:zoom="0.9"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import Grainient from "./Grainient.vue";
|
||||||
|
</script>`
|
||||||
|
});
|
||||||
@@ -9,7 +9,7 @@ export const gridDistortion = createCodeObject(code, 'Backgrounds/GridDistortion
|
|||||||
imageSrc="https://picsum.photos/1920/1080?grayscale"
|
imageSrc="https://picsum.photos/1920/1080?grayscale"
|
||||||
:grid="10"
|
:grid="10"
|
||||||
:mouse="0.1"
|
:mouse="0.1"
|
||||||
:strength="0.15
|
:strength="0.15"
|
||||||
:relaxation="0.9"
|
:relaxation="0.9"
|
||||||
className="custom-class"
|
className="custom-class"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,18 +1,15 @@
|
|||||||
import code from '@content/TextAnimations/BlurText/BlurText.vue?raw';
|
|
||||||
import { createCodeObject } from '@/types/code';
|
import { createCodeObject } from '@/types/code';
|
||||||
|
import code from '@content/TextAnimations/BlurText/BlurText.vue?raw';
|
||||||
|
|
||||||
export const blurText = createCodeObject(code, 'TextAnimations/BlurText', {
|
export const blurText = createCodeObject(code, 'TextAnimations/BlurText', {
|
||||||
installation: `npm install motion-v`,
|
installation: `npm install motion-v`,
|
||||||
usage: `<template>
|
usage: `<template>
|
||||||
<BlurText
|
<BlurText
|
||||||
text="Isn't this so cool?!"
|
text="Isn't this so cool?!"
|
||||||
:delay="200"
|
|
||||||
class-name="text-2xl font-semibold text-center"
|
|
||||||
animate-by="words"
|
animate-by="words"
|
||||||
direction="top"
|
direction="top"
|
||||||
:threshold="0.1"
|
:delay="200"
|
||||||
root-margin="0px"
|
class-name="text-2xl font-semibold text-center"
|
||||||
:step-duration="0.35"
|
|
||||||
@animation-complete="handleAnimationComplete"
|
@animation-complete="handleAnimationComplete"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,17 +1,18 @@
|
|||||||
import code from '@content/TextAnimations/FuzzyText/FuzzyText.vue?raw';
|
|
||||||
import { createCodeObject } from '@/types/code';
|
import { createCodeObject } from '@/types/code';
|
||||||
|
import code from '@content/TextAnimations/FuzzyText/FuzzyText.vue?raw';
|
||||||
|
|
||||||
export const fuzzyText = createCodeObject(code, 'TextAnimations/FuzzyText', {
|
export const fuzzyText = createCodeObject(code, 'TextAnimations/FuzzyText', {
|
||||||
usage: `<template>
|
usage: `<template>
|
||||||
<FuzzyText
|
<FuzzyText
|
||||||
text="404"
|
|
||||||
:font-size="140"
|
:font-size="140"
|
||||||
font-weight="900"
|
:font-weight="900"
|
||||||
color="#fff"
|
color="#fff"
|
||||||
:enable-hover="true"
|
:enable-hover="true"
|
||||||
:base-intensity="0.18"
|
:base-intensity="0.18"
|
||||||
:hover-intensity="0.5"
|
:hover-intensity="0.5"
|
||||||
/>
|
>
|
||||||
|
404
|
||||||
|
</FuzzyText>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|||||||
@@ -1,15 +1,18 @@
|
|||||||
import code from '@content/TextAnimations/GradientText/GradientText.vue?raw';
|
|
||||||
import { createCodeObject } from '@/types/code';
|
import { createCodeObject } from '@/types/code';
|
||||||
|
import code from '@content/TextAnimations/GradientText/GradientText.vue?raw';
|
||||||
|
|
||||||
export const gradientText = createCodeObject(code, 'TextAnimations/GradientText', {
|
export const gradientText = createCodeObject(code, 'TextAnimations/GradientText', {
|
||||||
usage: `<template>
|
installation: `npm install motion-v`,
|
||||||
|
usage: `// For a smoother animation, the gradient should start and end with the same color
|
||||||
|
<template>
|
||||||
<GradientText
|
<GradientText
|
||||||
text="Add a splash of color!"
|
:colors="['#27FF64', '#27FF64', '#A0FFBC']"
|
||||||
:colors="['#ffaa40', '#9c40ff', '#ffaa40']"
|
:animation-speed="3"
|
||||||
:animation-speed="8"
|
|
||||||
:show-border="false"
|
:show-border="false"
|
||||||
class-name="your-custom-class"
|
class-name="your-custom-class"
|
||||||
/>
|
>
|
||||||
|
Add a splash of color!
|
||||||
|
</GradientText>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|||||||
@@ -1,13 +1,20 @@
|
|||||||
import code from '@content/TextAnimations/ShinyText/ShinyText.vue?raw';
|
|
||||||
import { createCodeObject } from '@/types/code';
|
import { createCodeObject } from '@/types/code';
|
||||||
|
import code from '@content/TextAnimations/ShinyText/ShinyText.vue?raw';
|
||||||
|
|
||||||
export const shinyText = createCodeObject(code, 'TextAnimations/ShinyText', {
|
export const shinyText = createCodeObject(code, 'TextAnimations/ShinyText', {
|
||||||
|
installation: `npm install motion-v`,
|
||||||
usage: `<template>
|
usage: `<template>
|
||||||
<ShinyText
|
<ShinyText
|
||||||
text="Just some shiny text!"
|
text="✨ Shiny Text Effect"
|
||||||
|
:speed="2"
|
||||||
|
:delay="0.5"
|
||||||
:disabled="false"
|
:disabled="false"
|
||||||
:speed="3"
|
:color="'#b5b5b5'"
|
||||||
class-name="your-custom-class"
|
:shine-color="'#ffffff'"
|
||||||
|
:spread="120"
|
||||||
|
:direction="'left'"
|
||||||
|
:yoyo="false"
|
||||||
|
:pause-on-hover="false"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
377
src/content/Animations/OrbitImages/OrbitImages.vue
Normal file
377
src/content/Animations/OrbitImages/OrbitImages.vue
Normal file
@@ -0,0 +1,377 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
// Component created by Dominik Koch
|
||||||
|
// https://x.com/dominikkoch
|
||||||
|
|
||||||
|
import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||||
|
|
||||||
|
export type OrbitShape =
|
||||||
|
| 'ellipse'
|
||||||
|
| 'circle'
|
||||||
|
| 'square'
|
||||||
|
| 'rectangle'
|
||||||
|
| 'triangle'
|
||||||
|
| 'star'
|
||||||
|
| 'heart'
|
||||||
|
| 'infinity'
|
||||||
|
| 'wave'
|
||||||
|
| 'custom';
|
||||||
|
|
||||||
|
interface OrbitImagesProps {
|
||||||
|
images?: string[];
|
||||||
|
altPrefix?: string;
|
||||||
|
shape?: OrbitShape;
|
||||||
|
customPath?: string;
|
||||||
|
baseWidth?: number;
|
||||||
|
radiusX?: number;
|
||||||
|
radiusY?: number;
|
||||||
|
radius?: number;
|
||||||
|
starPoints?: number;
|
||||||
|
starInnerRatio?: number;
|
||||||
|
rotation?: number;
|
||||||
|
duration?: number;
|
||||||
|
itemSize?: number;
|
||||||
|
direction?: 'normal' | 'reverse';
|
||||||
|
fill?: boolean;
|
||||||
|
width?: number | '100%';
|
||||||
|
height?: number | 'auto';
|
||||||
|
className?: string;
|
||||||
|
showPath?: boolean;
|
||||||
|
pathColor?: string;
|
||||||
|
pathWidth?: number;
|
||||||
|
easing?: 'linear' | 'easeIn' | 'easeOut' | 'easeInOut';
|
||||||
|
paused?: boolean;
|
||||||
|
responsive?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<OrbitImagesProps>(), {
|
||||||
|
images: () => [],
|
||||||
|
altPrefix: 'Orbiting image',
|
||||||
|
shape: 'ellipse',
|
||||||
|
customPath: undefined,
|
||||||
|
baseWidth: 1400,
|
||||||
|
radiusX: 700,
|
||||||
|
radiusY: 170,
|
||||||
|
radius: 300,
|
||||||
|
starPoints: 5,
|
||||||
|
starInnerRatio: 0.5,
|
||||||
|
rotation: -8,
|
||||||
|
duration: 40,
|
||||||
|
itemSize: 64,
|
||||||
|
direction: 'normal',
|
||||||
|
fill: true,
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
className: '',
|
||||||
|
showPath: false,
|
||||||
|
pathColor: 'rgba(0,0,0,0.1)',
|
||||||
|
pathWidth: 2,
|
||||||
|
easing: 'linear',
|
||||||
|
paused: false,
|
||||||
|
responsive: false
|
||||||
|
});
|
||||||
|
|
||||||
|
const containerRef = ref<HTMLDivElement | null>(null);
|
||||||
|
const scale = ref(1);
|
||||||
|
const progress = ref(0);
|
||||||
|
let animationFrameId: number | null = null;
|
||||||
|
let lastTime: number | null = null;
|
||||||
|
|
||||||
|
const designCenterX = computed(() => props.baseWidth / 2);
|
||||||
|
const designCenterY = computed(() => props.baseWidth / 2);
|
||||||
|
|
||||||
|
function generateEllipsePath(cx: number, cy: number, rx: number, ry: number): string {
|
||||||
|
return `M ${cx - rx} ${cy} A ${rx} ${ry} 0 1 0 ${cx + rx} ${cy} A ${rx} ${ry} 0 1 0 ${cx - rx} ${cy}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateCirclePath(cx: number, cy: number, r: number): string {
|
||||||
|
return generateEllipsePath(cx, cy, r, r);
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateSquarePath(cx: number, cy: number, size: number): string {
|
||||||
|
const h = size / 2;
|
||||||
|
return `M ${cx - h} ${cy - h} L ${cx + h} ${cy - h} L ${cx + h} ${cy + h} L ${cx - h} ${cy + h} Z`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateRectanglePath(cx: number, cy: number, w: number, h: number): string {
|
||||||
|
const hw = w / 2;
|
||||||
|
const hh = h / 2;
|
||||||
|
return `M ${cx - hw} ${cy - hh} L ${cx + hw} ${cy - hh} L ${cx + hw} ${cy + hh} L ${cx - hw} ${cy + hh} Z`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateTrianglePath(cx: number, cy: number, size: number): string {
|
||||||
|
const height = (size * Math.sqrt(3)) / 2;
|
||||||
|
const hs = size / 2;
|
||||||
|
return `M ${cx} ${cy - height / 1.5} L ${cx + hs} ${cy + height / 3} L ${cx - hs} ${cy + height / 3} Z`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateStarPath(cx: number, cy: number, outerR: number, innerR: number, points: number): string {
|
||||||
|
const step = Math.PI / points;
|
||||||
|
let path = '';
|
||||||
|
for (let i = 0; i < 2 * points; i++) {
|
||||||
|
const r = i % 2 === 0 ? outerR : innerR;
|
||||||
|
const angle = i * step - Math.PI / 2;
|
||||||
|
const x = cx + r * Math.cos(angle);
|
||||||
|
const y = cy + r * Math.sin(angle);
|
||||||
|
path += i === 0 ? `M ${x} ${y}` : ` L ${x} ${y}`;
|
||||||
|
}
|
||||||
|
return path + ' Z';
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateHeartPath(cx: number, cy: number, size: number): string {
|
||||||
|
const s = size / 30;
|
||||||
|
return `M ${cx} ${cy + 12 * s} C ${cx - 20 * s} ${cy - 5 * s}, ${cx - 12 * s} ${cy - 18 * s}, ${cx} ${cy - 8 * s} C ${cx + 12 * s} ${cy - 18 * s}, ${cx + 20 * s} ${cy - 5 * s}, ${cx} ${cy + 12 * s}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateInfinityPath(cx: number, cy: number, w: number, h: number): string {
|
||||||
|
const hw = w / 2;
|
||||||
|
const hh = h / 2;
|
||||||
|
return `M ${cx} ${cy} C ${cx + hw * 0.5} ${cy - hh}, ${cx + hw} ${cy - hh}, ${cx + hw} ${cy} C ${cx + hw} ${cy + hh}, ${cx + hw * 0.5} ${cy + hh}, ${cx} ${cy} C ${cx - hw * 0.5} ${cy + hh}, ${cx - hw} ${cy + hh}, ${cx - hw} ${cy} C ${cx - hw} ${cy - hh}, ${cx - hw * 0.5} ${cy - hh}, ${cx} ${cy}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateWavePath(cx: number, cy: number, w: number, amplitude: number, waves: number): string {
|
||||||
|
const pts: string[] = [];
|
||||||
|
const segs = waves * 20;
|
||||||
|
const hw = w / 2;
|
||||||
|
for (let i = 0; i <= segs; i++) {
|
||||||
|
const x = cx - hw + (w * i) / segs;
|
||||||
|
const y = cy + Math.sin((i / segs) * waves * 2 * Math.PI) * amplitude;
|
||||||
|
pts.push(i === 0 ? `M ${x} ${y}` : `L ${x} ${y}`);
|
||||||
|
}
|
||||||
|
for (let i = segs; i >= 0; i--) {
|
||||||
|
const x = cx - hw + (w * i) / segs;
|
||||||
|
const y = cy - Math.sin((i / segs) * waves * 2 * Math.PI) * amplitude;
|
||||||
|
pts.push(`L ${x} ${y}`);
|
||||||
|
}
|
||||||
|
return pts.join(' ') + ' Z';
|
||||||
|
}
|
||||||
|
|
||||||
|
const path = computed(() => {
|
||||||
|
const cx = designCenterX.value;
|
||||||
|
const cy = designCenterY.value;
|
||||||
|
|
||||||
|
switch (props.shape) {
|
||||||
|
case 'circle':
|
||||||
|
return generateCirclePath(cx, cy, props.radius);
|
||||||
|
case 'ellipse':
|
||||||
|
return generateEllipsePath(cx, cy, props.radiusX, props.radiusY);
|
||||||
|
case 'square':
|
||||||
|
return generateSquarePath(cx, cy, props.radius * 2);
|
||||||
|
case 'rectangle':
|
||||||
|
return generateRectanglePath(cx, cy, props.radiusX * 2, props.radiusY * 2);
|
||||||
|
case 'triangle':
|
||||||
|
return generateTrianglePath(cx, cy, props.radius * 2);
|
||||||
|
case 'star':
|
||||||
|
return generateStarPath(cx, cy, props.radius, props.radius * props.starInnerRatio, props.starPoints);
|
||||||
|
case 'heart':
|
||||||
|
return generateHeartPath(cx, cy, props.radius * 2);
|
||||||
|
case 'infinity':
|
||||||
|
return generateInfinityPath(cx, cy, props.radiusX * 2, props.radiusY * 2);
|
||||||
|
case 'wave':
|
||||||
|
return generateWavePath(cx, cy, props.radiusX * 2, props.radiusY, 3);
|
||||||
|
case 'custom':
|
||||||
|
return props.customPath || generateCirclePath(cx, cy, props.radius);
|
||||||
|
default:
|
||||||
|
return generateEllipsePath(cx, cy, props.radiusX, props.radiusY);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const containerWidth = computed(() => {
|
||||||
|
return props.responsive ? '100%' : (typeof props.width === 'number' ? `${props.width}px` : '100%');
|
||||||
|
});
|
||||||
|
|
||||||
|
const containerHeight = computed(() => {
|
||||||
|
return props.responsive ? 'auto' : (typeof props.height === 'number' ? `${props.height}px` : (typeof props.width === 'number' ? `${props.width}px` : 'auto'));
|
||||||
|
});
|
||||||
|
|
||||||
|
function updateScale() {
|
||||||
|
if (!props.responsive || !containerRef.value) return;
|
||||||
|
scale.value = containerRef.value.clientWidth / props.baseWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyEasing(t: number): number {
|
||||||
|
switch (props.easing) {
|
||||||
|
case 'easeIn':
|
||||||
|
return t * t;
|
||||||
|
case 'easeOut':
|
||||||
|
return 1 - (1 - t) * (1 - t);
|
||||||
|
case 'easeInOut':
|
||||||
|
return t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2;
|
||||||
|
case 'linear':
|
||||||
|
default:
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function animationLoop(currentTime: number) {
|
||||||
|
if (props.paused) {
|
||||||
|
lastTime = null;
|
||||||
|
animationFrameId = requestAnimationFrame(animationLoop);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastTime === null) {
|
||||||
|
lastTime = currentTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
const deltaTime = (currentTime - lastTime) / 1000;
|
||||||
|
lastTime = currentTime;
|
||||||
|
|
||||||
|
const progressPerSecond = 100 / props.duration;
|
||||||
|
const delta = props.direction === 'reverse' ? -progressPerSecond * deltaTime : progressPerSecond * deltaTime;
|
||||||
|
|
||||||
|
progress.value = ((progress.value + delta) % 100 + 100) % 100;
|
||||||
|
|
||||||
|
animationFrameId = requestAnimationFrame(animationLoop);
|
||||||
|
}
|
||||||
|
|
||||||
|
function startAnimation() {
|
||||||
|
stopAnimation();
|
||||||
|
lastTime = null;
|
||||||
|
animationFrameId = requestAnimationFrame(animationLoop);
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopAnimation() {
|
||||||
|
if (animationFrameId !== null) {
|
||||||
|
cancelAnimationFrame(animationFrameId);
|
||||||
|
animationFrameId = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOffsetDistance(index: number): string {
|
||||||
|
const itemOffset = props.fill ? (index / props.images.length) * 100 : 0;
|
||||||
|
const offset = (((progress.value + itemOffset) % 100) + 100) % 100;
|
||||||
|
return `${offset}%`;
|
||||||
|
}
|
||||||
|
|
||||||
|
let resizeObserver: ResizeObserver | null = null;
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
updateScale();
|
||||||
|
startAnimation();
|
||||||
|
|
||||||
|
if (props.responsive && containerRef.value) {
|
||||||
|
resizeObserver = new ResizeObserver(updateScale);
|
||||||
|
resizeObserver.observe(containerRef.value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
stopAnimation();
|
||||||
|
if (resizeObserver) {
|
||||||
|
resizeObserver.disconnect();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [props.duration, props.direction, props.paused],
|
||||||
|
() => {
|
||||||
|
if (!props.paused && animationFrameId === null) {
|
||||||
|
startAnimation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.responsive,
|
||||||
|
(newVal) => {
|
||||||
|
if (newVal && containerRef.value) {
|
||||||
|
updateScale();
|
||||||
|
if (!resizeObserver) {
|
||||||
|
resizeObserver = new ResizeObserver(updateScale);
|
||||||
|
resizeObserver.observe(containerRef.value);
|
||||||
|
}
|
||||||
|
} else if (resizeObserver) {
|
||||||
|
resizeObserver.disconnect();
|
||||||
|
resizeObserver = null;
|
||||||
|
scale.value = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const offsetDistances = computed(() => {
|
||||||
|
return props.images.map((_, index) => {
|
||||||
|
const itemOffset = props.fill ? (index / props.images.length) * 100 : 0;
|
||||||
|
const offset = (((progress.value + itemOffset) % 100) + 100) % 100;
|
||||||
|
return `${offset}%`;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
ref="containerRef"
|
||||||
|
:class="`relative mx-auto ${props.className}`"
|
||||||
|
:style="{
|
||||||
|
width: containerWidth,
|
||||||
|
height: containerHeight,
|
||||||
|
aspectRatio: props.responsive ? '1 / 1' : undefined
|
||||||
|
}"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
:class="props.responsive ? 'absolute left-1/2 top-1/2' : 'relative w-full h-full'"
|
||||||
|
:style="{
|
||||||
|
width: props.responsive ? `${props.baseWidth}px` : '100%',
|
||||||
|
height: props.responsive ? `${props.baseWidth}px` : '100%',
|
||||||
|
transform: props.responsive ? `translate(-50%, -50%) scale(${scale})` : undefined,
|
||||||
|
transformOrigin: 'center center'
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="relative w-full h-full"
|
||||||
|
:style="{
|
||||||
|
transform: `rotate(${props.rotation}deg)`,
|
||||||
|
transformOrigin: 'center center'
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<!-- SVG Path for debugging -->
|
||||||
|
<svg
|
||||||
|
v-if="props.showPath"
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
|
:viewBox="`0 0 ${props.baseWidth} ${props.baseWidth}`"
|
||||||
|
class="absolute inset-0 pointer-events-none"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
:d="path"
|
||||||
|
fill="none"
|
||||||
|
:stroke="props.pathColor"
|
||||||
|
:stroke-width="props.pathWidth / scale"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<!-- Orbit Items -->
|
||||||
|
<div
|
||||||
|
v-for="(src, index) in props.images"
|
||||||
|
:key="src"
|
||||||
|
class="absolute will-change-transform select-none"
|
||||||
|
:style="{
|
||||||
|
width: `${props.itemSize}px`,
|
||||||
|
height: `${props.itemSize}px`,
|
||||||
|
offsetPath: `path('${path}')`,
|
||||||
|
offsetRotate: '0deg',
|
||||||
|
offsetAnchor: 'center center',
|
||||||
|
offsetDistance: offsetDistances[index]
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div :style="{ transform: `rotate(${-props.rotation}deg)` }">
|
||||||
|
<img
|
||||||
|
:src="src"
|
||||||
|
:alt="`${props.altPrefix} ${index + 1}`"
|
||||||
|
:draggable="false"
|
||||||
|
class="w-full h-full object-contain"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Center Content Slot -->
|
||||||
|
<div
|
||||||
|
v-if="$slots.centerContent"
|
||||||
|
class="absolute inset-0 flex items-center justify-center z-10"
|
||||||
|
>
|
||||||
|
<slot name="centerContent" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
285
src/content/Backgrounds/Grainient/Grainient.vue
Normal file
285
src/content/Backgrounds/Grainient/Grainient.vue
Normal file
@@ -0,0 +1,285 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { Mesh, Program, Renderer, Triangle } from 'ogl';
|
||||||
|
import { onBeforeUnmount, onMounted, useTemplateRef, watch } from 'vue';
|
||||||
|
|
||||||
|
interface GrainientProps {
|
||||||
|
timeSpeed?: number;
|
||||||
|
colorBalance?: number;
|
||||||
|
warpStrength?: number;
|
||||||
|
warpFrequency?: number;
|
||||||
|
warpSpeed?: number;
|
||||||
|
warpAmplitude?: number;
|
||||||
|
blendAngle?: number;
|
||||||
|
blendSoftness?: number;
|
||||||
|
rotationAmount?: number;
|
||||||
|
noiseScale?: number;
|
||||||
|
grainAmount?: number;
|
||||||
|
grainScale?: number;
|
||||||
|
grainAnimated?: boolean;
|
||||||
|
contrast?: number;
|
||||||
|
gamma?: number;
|
||||||
|
saturation?: number;
|
||||||
|
centerX?: number;
|
||||||
|
centerY?: number;
|
||||||
|
zoom?: number;
|
||||||
|
color1?: string;
|
||||||
|
color2?: string;
|
||||||
|
color3?: string;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hexToRgb = (hex: string): [number, number, number] => {
|
||||||
|
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
||||||
|
if (!result) return [1, 1, 1];
|
||||||
|
return [parseInt(result[1], 16) / 255, parseInt(result[2], 16) / 255, parseInt(result[3], 16) / 255];
|
||||||
|
};
|
||||||
|
|
||||||
|
const vertex = `#version 300 es
|
||||||
|
in vec2 position;
|
||||||
|
void main() {
|
||||||
|
gl_Position = vec4(position, 0.0, 1.0);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const fragment = `#version 300 es
|
||||||
|
precision highp float;
|
||||||
|
uniform vec2 iResolution;
|
||||||
|
uniform float iTime;
|
||||||
|
uniform float uTimeSpeed;
|
||||||
|
uniform float uColorBalance;
|
||||||
|
uniform float uWarpStrength;
|
||||||
|
uniform float uWarpFrequency;
|
||||||
|
uniform float uWarpSpeed;
|
||||||
|
uniform float uWarpAmplitude;
|
||||||
|
uniform float uBlendAngle;
|
||||||
|
uniform float uBlendSoftness;
|
||||||
|
uniform float uRotationAmount;
|
||||||
|
uniform float uNoiseScale;
|
||||||
|
uniform float uGrainAmount;
|
||||||
|
uniform float uGrainScale;
|
||||||
|
uniform float uGrainAnimated;
|
||||||
|
uniform float uContrast;
|
||||||
|
uniform float uGamma;
|
||||||
|
uniform float uSaturation;
|
||||||
|
uniform vec2 uCenterOffset;
|
||||||
|
uniform float uZoom;
|
||||||
|
uniform vec3 uColor1;
|
||||||
|
uniform vec3 uColor2;
|
||||||
|
uniform vec3 uColor3;
|
||||||
|
out vec4 fragColor;
|
||||||
|
#define S(a,b,t) smoothstep(a,b,t)
|
||||||
|
mat2 Rot(float a){float s=sin(a),c=cos(a);return mat2(c,-s,s,c);}
|
||||||
|
vec2 hash(vec2 p){p=vec2(dot(p,vec2(2127.1,81.17)),dot(p,vec2(1269.5,283.37)));return fract(sin(p)*43758.5453);}
|
||||||
|
float noise(vec2 p){vec2 i=floor(p),f=fract(p),u=f*f*(3.0-2.0*f);float n=mix(mix(dot(-1.0+2.0*hash(i+vec2(0.0,0.0)),f-vec2(0.0,0.0)),dot(-1.0+2.0*hash(i+vec2(1.0,0.0)),f-vec2(1.0,0.0)),u.x),mix(dot(-1.0+2.0*hash(i+vec2(0.0,1.0)),f-vec2(0.0,1.0)),dot(-1.0+2.0*hash(i+vec2(1.0,1.0)),f-vec2(1.0,1.0)),u.x),u.y);return 0.5+0.5*n;}
|
||||||
|
void mainImage(out vec4 o, vec2 C){
|
||||||
|
float t=iTime*uTimeSpeed;
|
||||||
|
vec2 uv=C/iResolution.xy;
|
||||||
|
float ratio=iResolution.x/iResolution.y;
|
||||||
|
vec2 tuv=uv-0.5+uCenterOffset;
|
||||||
|
tuv/=max(uZoom,0.001);
|
||||||
|
|
||||||
|
float degree=noise(vec2(t*0.1,tuv.x*tuv.y)*uNoiseScale);
|
||||||
|
tuv.y*=1.0/ratio;
|
||||||
|
tuv*=Rot(radians((degree-0.5)*uRotationAmount+180.0));
|
||||||
|
tuv.y*=ratio;
|
||||||
|
|
||||||
|
float frequency=uWarpFrequency;
|
||||||
|
float ws=max(uWarpStrength,0.001);
|
||||||
|
float amplitude=uWarpAmplitude/ws;
|
||||||
|
float warpTime=t*uWarpSpeed;
|
||||||
|
tuv.x+=sin(tuv.y*frequency+warpTime)/amplitude;
|
||||||
|
tuv.y+=sin(tuv.x*(frequency*1.5)+warpTime)/(amplitude*0.5);
|
||||||
|
|
||||||
|
vec3 colLav=uColor1;
|
||||||
|
vec3 colOrg=uColor2;
|
||||||
|
vec3 colDark=uColor3;
|
||||||
|
float b=uColorBalance;
|
||||||
|
float s=max(uBlendSoftness,0.0);
|
||||||
|
mat2 blendRot=Rot(radians(uBlendAngle));
|
||||||
|
float blendX=(tuv*blendRot).x;
|
||||||
|
float edge0=-0.3-b-s;
|
||||||
|
float edge1=0.2-b+s;
|
||||||
|
float v0=0.5-b+s;
|
||||||
|
float v1=-0.3-b-s;
|
||||||
|
vec3 layer1=mix(colDark,colOrg,S(edge0,edge1,blendX));
|
||||||
|
vec3 layer2=mix(colOrg,colLav,S(edge0,edge1,blendX));
|
||||||
|
vec3 col=mix(layer1,layer2,S(v0,v1,tuv.y));
|
||||||
|
|
||||||
|
vec2 grainUv=uv*max(uGrainScale,0.001);
|
||||||
|
if(uGrainAnimated>0.5){grainUv+=vec2(iTime*0.05);}
|
||||||
|
float grain=fract(sin(dot(grainUv,vec2(12.9898,78.233)))*43758.5453);
|
||||||
|
col+=(grain-0.5)*uGrainAmount;
|
||||||
|
|
||||||
|
col=(col-0.5)*uContrast+0.5;
|
||||||
|
float luma=dot(col,vec3(0.2126,0.7152,0.0722));
|
||||||
|
col=mix(vec3(luma),col,uSaturation);
|
||||||
|
col=pow(max(col,0.0),vec3(1.0/max(uGamma,0.001)));
|
||||||
|
col=clamp(col,0.0,1.0);
|
||||||
|
|
||||||
|
o=vec4(col,1.0);
|
||||||
|
}
|
||||||
|
void main(){
|
||||||
|
vec4 o=vec4(0.0);
|
||||||
|
mainImage(o,gl_FragCoord.xy);
|
||||||
|
fragColor=o;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<GrainientProps>(), {
|
||||||
|
timeSpeed: 0.25,
|
||||||
|
colorBalance: 0.0,
|
||||||
|
warpStrength: 1.0,
|
||||||
|
warpFrequency: 5.0,
|
||||||
|
warpSpeed: 2.0,
|
||||||
|
warpAmplitude: 50.0,
|
||||||
|
blendAngle: 0.0,
|
||||||
|
blendSoftness: 0.05,
|
||||||
|
rotationAmount: 500.0,
|
||||||
|
noiseScale: 2.0,
|
||||||
|
grainAmount: 0.1,
|
||||||
|
grainScale: 2.0,
|
||||||
|
grainAnimated: false,
|
||||||
|
contrast: 1.5,
|
||||||
|
gamma: 1.0,
|
||||||
|
saturation: 1.0,
|
||||||
|
centerX: 0.0,
|
||||||
|
centerY: 0.0,
|
||||||
|
zoom: 0.9,
|
||||||
|
color1: '#FF9FFC',
|
||||||
|
color2: '#5227FF',
|
||||||
|
color3: '#B19EEF',
|
||||||
|
className: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
const containerRef = useTemplateRef<HTMLDivElement>('containerRef');
|
||||||
|
|
||||||
|
let cleanup: (() => void) | null = null;
|
||||||
|
const setup = () => {
|
||||||
|
if (!containerRef.value) return;
|
||||||
|
|
||||||
|
const renderer = new Renderer({
|
||||||
|
webgl: 2,
|
||||||
|
alpha: true,
|
||||||
|
antialias: false,
|
||||||
|
dpr: Math.min(window.devicePixelRatio || 1, 2)
|
||||||
|
});
|
||||||
|
|
||||||
|
const gl = renderer.gl;
|
||||||
|
const canvas = gl.canvas as HTMLCanvasElement;
|
||||||
|
canvas.style.width = '100%';
|
||||||
|
canvas.style.height = '100%';
|
||||||
|
canvas.style.display = 'block';
|
||||||
|
|
||||||
|
const container = containerRef.value;
|
||||||
|
container.appendChild(canvas);
|
||||||
|
|
||||||
|
const geometry = new Triangle(gl);
|
||||||
|
const program = new Program(gl, {
|
||||||
|
vertex,
|
||||||
|
fragment,
|
||||||
|
uniforms: {
|
||||||
|
iTime: { value: 0 },
|
||||||
|
iResolution: { value: new Float32Array([1, 1]) },
|
||||||
|
uTimeSpeed: { value: props.timeSpeed },
|
||||||
|
uColorBalance: { value: props.colorBalance },
|
||||||
|
uWarpStrength: { value: props.warpStrength },
|
||||||
|
uWarpFrequency: { value: props.warpFrequency },
|
||||||
|
uWarpSpeed: { value: props.warpSpeed },
|
||||||
|
uWarpAmplitude: { value: props.warpAmplitude },
|
||||||
|
uBlendAngle: { value: props.blendAngle },
|
||||||
|
uBlendSoftness: { value: props.blendSoftness },
|
||||||
|
uRotationAmount: { value: props.rotationAmount },
|
||||||
|
uNoiseScale: { value: props.noiseScale },
|
||||||
|
uGrainAmount: { value: props.grainAmount },
|
||||||
|
uGrainScale: { value: props.grainScale },
|
||||||
|
uGrainAnimated: { value: props.grainAnimated ? 1.0 : 0.0 },
|
||||||
|
uContrast: { value: props.contrast },
|
||||||
|
uGamma: { value: props.gamma },
|
||||||
|
uSaturation: { value: props.saturation },
|
||||||
|
uCenterOffset: { value: new Float32Array([props.centerX, props.centerY]) },
|
||||||
|
uZoom: { value: props.zoom },
|
||||||
|
uColor1: { value: new Float32Array(hexToRgb(props.color1)) },
|
||||||
|
uColor2: { value: new Float32Array(hexToRgb(props.color2)) },
|
||||||
|
uColor3: { value: new Float32Array(hexToRgb(props.color3)) }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const mesh = new Mesh(gl, { geometry, program });
|
||||||
|
|
||||||
|
const setSize = () => {
|
||||||
|
const rect = container.getBoundingClientRect();
|
||||||
|
const width = Math.max(1, Math.floor(rect.width));
|
||||||
|
const height = Math.max(1, Math.floor(rect.height));
|
||||||
|
renderer.setSize(width, height);
|
||||||
|
const res = (program.uniforms.iResolution as { value: Float32Array }).value;
|
||||||
|
res[0] = gl.drawingBufferWidth;
|
||||||
|
res[1] = gl.drawingBufferHeight;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ro = new ResizeObserver(setSize);
|
||||||
|
ro.observe(container);
|
||||||
|
setSize();
|
||||||
|
|
||||||
|
let raf = 0;
|
||||||
|
const t0 = performance.now();
|
||||||
|
const loop = (t: number) => {
|
||||||
|
(program.uniforms.iTime as { value: number }).value = (t - t0) * 0.001;
|
||||||
|
renderer.render({ scene: mesh });
|
||||||
|
raf = requestAnimationFrame(loop);
|
||||||
|
};
|
||||||
|
raf = requestAnimationFrame(loop);
|
||||||
|
|
||||||
|
cleanup = () => {
|
||||||
|
cancelAnimationFrame(raf);
|
||||||
|
ro.disconnect();
|
||||||
|
try {
|
||||||
|
container.removeChild(canvas);
|
||||||
|
} catch {
|
||||||
|
// Ignore
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(setup);
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
cleanup?.();
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [
|
||||||
|
props.timeSpeed,
|
||||||
|
props.colorBalance,
|
||||||
|
props.warpStrength,
|
||||||
|
props.warpFrequency,
|
||||||
|
props.warpSpeed,
|
||||||
|
props.warpAmplitude,
|
||||||
|
props.blendAngle,
|
||||||
|
props.blendSoftness,
|
||||||
|
props.rotationAmount,
|
||||||
|
props.noiseScale,
|
||||||
|
props.grainAmount,
|
||||||
|
props.grainScale,
|
||||||
|
props.grainAnimated,
|
||||||
|
props.contrast,
|
||||||
|
props.gamma,
|
||||||
|
props.saturation,
|
||||||
|
props.centerX,
|
||||||
|
props.centerY,
|
||||||
|
props.zoom,
|
||||||
|
props.color1,
|
||||||
|
props.color2,
|
||||||
|
props.color3
|
||||||
|
],
|
||||||
|
() => {
|
||||||
|
cleanup?.();
|
||||||
|
setup();
|
||||||
|
},
|
||||||
|
{
|
||||||
|
deep: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div ref="containerRef" :class="['relative h-full w-full overflow-hidden', className]" />
|
||||||
|
</template>
|
||||||
@@ -1,81 +1,99 @@
|
|||||||
<template>
|
<template>
|
||||||
<p ref="rootRef" :class="['blur-text', className, 'flex', 'flex-wrap']">
|
<p ref="rootRef" class="flex flex-wrap blur-text" :class="className">
|
||||||
<Motion
|
<Motion
|
||||||
v-for="(segment, index) in elements"
|
v-for="(segment, index) in elements"
|
||||||
:key="`${animationKey}-${index}`"
|
:key="index"
|
||||||
tag="span"
|
tag="span"
|
||||||
:initial="fromSnapshot"
|
:initial="fromSnapshot"
|
||||||
:animate="inView ? getAnimateKeyframes() : fromSnapshot"
|
:animate="inView ? buildKeyframes(fromSnapshot, toSnapshots) : fromSnapshot"
|
||||||
:transition="getTransition(index)"
|
:transition="getTransition(index)"
|
||||||
:style="{
|
@animation-complete="handleAnimationComplete(index)"
|
||||||
display: 'inline-block',
|
style="display: inline-block; will-change: transform, filter, opacity"
|
||||||
willChange: 'transform, filter, opacity'
|
|
||||||
}"
|
|
||||||
@animation-complete="() => handleAnimationComplete(index)"
|
|
||||||
>
|
>
|
||||||
{{ segment === ' ' ? '\u00A0' : segment
|
{{ segment === ' ' ? '\u00A0' : segment }}
|
||||||
}}{{ animateBy === 'words' && index < elements.length - 1 ? '\u00A0' : '' }}
|
<template v-if="animateBy === 'words' && index < elements.length - 1"> </template>
|
||||||
</Motion>
|
</Motion>
|
||||||
</p>
|
</p>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted, onUnmounted, watch, useTemplateRef } from 'vue';
|
import { Motion, type Transition } from 'motion-v';
|
||||||
import { Motion } from 'motion-v';
|
import { computed, onBeforeUnmount, onMounted, ref, useTemplateRef } from 'vue';
|
||||||
|
|
||||||
type AnimateBy = 'words' | 'letters';
|
type BlurTextProps = {
|
||||||
type Direction = 'top' | 'bottom';
|
|
||||||
type AnimationSnapshot = Record<string, string | number>;
|
|
||||||
|
|
||||||
interface BlurTextProps {
|
|
||||||
text?: string;
|
text?: string;
|
||||||
delay?: number;
|
delay?: number;
|
||||||
className?: string;
|
className?: string;
|
||||||
animateBy?: AnimateBy;
|
animateBy?: 'words' | 'letters';
|
||||||
direction?: Direction;
|
direction?: 'top' | 'bottom';
|
||||||
threshold?: number;
|
threshold?: number;
|
||||||
rootMargin?: string;
|
rootMargin?: string;
|
||||||
animationFrom?: AnimationSnapshot;
|
animationFrom?: Record<string, string | number>;
|
||||||
animationTo?: AnimationSnapshot[];
|
animationTo?: Array<Record<string, string | number>>;
|
||||||
easing?: (t: number) => number;
|
easing?: (t: number) => number;
|
||||||
onAnimationComplete?: () => void;
|
onAnimationComplete?: () => void;
|
||||||
stepDuration?: number;
|
stepDuration?: number;
|
||||||
}
|
};
|
||||||
|
|
||||||
|
const buildKeyframes = (
|
||||||
|
from: Record<string, string | number>,
|
||||||
|
steps: Array<Record<string, string | number>>
|
||||||
|
): Record<string, Array<string | number>> => {
|
||||||
|
const keys = new Set<string>([...Object.keys(from), ...steps.flatMap(s => Object.keys(s))]);
|
||||||
|
|
||||||
|
const keyframes: Record<string, Array<string | number>> = {};
|
||||||
|
keys.forEach(k => {
|
||||||
|
keyframes[k] = [from[k], ...steps.map(s => s[k])];
|
||||||
|
});
|
||||||
|
return keyframes;
|
||||||
|
};
|
||||||
|
|
||||||
const props = withDefaults(defineProps<BlurTextProps>(), {
|
const props = withDefaults(defineProps<BlurTextProps>(), {
|
||||||
text: '',
|
text: '',
|
||||||
delay: 200,
|
delay: 200,
|
||||||
className: '',
|
className: '',
|
||||||
animateBy: 'words' as AnimateBy,
|
animateBy: 'words',
|
||||||
direction: 'top' as Direction,
|
direction: 'top',
|
||||||
threshold: 0.1,
|
threshold: 0.1,
|
||||||
rootMargin: '0px',
|
rootMargin: '0px',
|
||||||
easing: (t: number) => t,
|
easing: (t: number) => t,
|
||||||
stepDuration: 0.35
|
stepDuration: 0.35
|
||||||
});
|
});
|
||||||
|
|
||||||
const buildKeyframes = (
|
const inView = ref(false);
|
||||||
from: AnimationSnapshot,
|
const rootRef = useTemplateRef<HTMLParagraphElement>('rootRef');
|
||||||
steps: AnimationSnapshot[]
|
let observer: IntersectionObserver | null = null;
|
||||||
): Record<string, Array<string | number>> => {
|
|
||||||
const keys = new Set<string>([...Object.keys(from), ...steps.flatMap(step => Object.keys(step))]);
|
|
||||||
|
|
||||||
const keyframes: Record<string, Array<string | number>> = {};
|
onMounted(() => {
|
||||||
|
if (!rootRef.value) return;
|
||||||
|
|
||||||
for (const key of keys) {
|
observer = new IntersectionObserver(
|
||||||
keyframes[key] = [from[key], ...steps.map(step => step[key])];
|
([entry]) => {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
inView.value = true;
|
||||||
|
observer?.unobserve(rootRef.value as Element);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
threshold: props.threshold,
|
||||||
|
rootMargin: props.rootMargin
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return keyframes;
|
observer.observe(rootRef.value);
|
||||||
};
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
observer?.disconnect();
|
||||||
|
});
|
||||||
|
|
||||||
const elements = computed(() => (props.animateBy === 'words' ? props.text.split(' ') : props.text.split('')));
|
const elements = computed(() => (props.animateBy === 'words' ? props.text.split(' ') : props.text.split('')));
|
||||||
|
|
||||||
const defaultFrom = computed<AnimationSnapshot>(() =>
|
const defaultFrom = computed(() =>
|
||||||
props.direction === 'top' ? { filter: 'blur(10px)', opacity: 0, y: -50 } : { filter: 'blur(10px)', opacity: 0, y: 50 }
|
props.direction === 'top' ? { filter: 'blur(10px)', opacity: 0, y: -50 } : { filter: 'blur(10px)', opacity: 0, y: 50 }
|
||||||
);
|
);
|
||||||
|
|
||||||
const defaultTo = computed<AnimationSnapshot[]>(() => [
|
const defaultTo = computed(() => [
|
||||||
{
|
{
|
||||||
filter: 'blur(5px)',
|
filter: 'blur(5px)',
|
||||||
opacity: 0.5,
|
opacity: 0.5,
|
||||||
@@ -93,71 +111,21 @@ const toSnapshots = computed(() => props.animationTo ?? defaultTo.value);
|
|||||||
|
|
||||||
const stepCount = computed(() => toSnapshots.value.length + 1);
|
const stepCount = computed(() => toSnapshots.value.length + 1);
|
||||||
const totalDuration = computed(() => props.stepDuration * (stepCount.value - 1));
|
const totalDuration = computed(() => props.stepDuration * (stepCount.value - 1));
|
||||||
|
|
||||||
const times = computed(() =>
|
const times = computed(() =>
|
||||||
Array.from({ length: stepCount.value }, (_, i) => (stepCount.value === 1 ? 0 : i / (stepCount.value - 1)))
|
Array.from({ length: stepCount.value }, (_, i) => (stepCount.value === 1 ? 0 : i / (stepCount.value - 1)))
|
||||||
);
|
);
|
||||||
|
|
||||||
const inView = ref(false);
|
const getTransition = (index: number): Transition => ({
|
||||||
const animationKey = ref(0);
|
|
||||||
const completionFired = ref(false);
|
|
||||||
const rootRef = useTemplateRef<HTMLParagraphElement>('rootRef');
|
|
||||||
|
|
||||||
let observer: IntersectionObserver | null = null;
|
|
||||||
|
|
||||||
const setupObserver = () => {
|
|
||||||
if (!rootRef.value) return;
|
|
||||||
|
|
||||||
observer = new IntersectionObserver(
|
|
||||||
([entry]) => {
|
|
||||||
if (entry.isIntersecting) {
|
|
||||||
inView.value = true;
|
|
||||||
observer?.unobserve(rootRef.value as Element);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
threshold: props.threshold,
|
|
||||||
rootMargin: props.rootMargin
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
observer.observe(rootRef.value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getAnimateKeyframes = () => {
|
|
||||||
return buildKeyframes(fromSnapshot.value, toSnapshots.value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getTransition = (index: number) => {
|
|
||||||
return {
|
|
||||||
duration: totalDuration.value,
|
duration: totalDuration.value,
|
||||||
times: times.value,
|
times: times.value,
|
||||||
delay: (index * props.delay) / 1000,
|
delay: (index * props.delay) / 1000,
|
||||||
ease: props.easing
|
ease: props.easing
|
||||||
};
|
});
|
||||||
};
|
|
||||||
|
|
||||||
const handleAnimationComplete = (index: number) => {
|
const handleAnimationComplete = (index: number) => {
|
||||||
if (index === elements.value.length - 1 && !completionFired.value && props.onAnimationComplete) {
|
if (index === elements.value.length - 1) {
|
||||||
completionFired.value = true;
|
props.onAnimationComplete?.();
|
||||||
props.onAnimationComplete();
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
setupObserver();
|
|
||||||
});
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
observer?.disconnect();
|
|
||||||
});
|
|
||||||
|
|
||||||
watch([() => props.threshold, () => props.rootMargin], () => {
|
|
||||||
observer?.disconnect();
|
|
||||||
setupObserver();
|
|
||||||
});
|
|
||||||
|
|
||||||
watch([() => props.delay, () => props.stepDuration, () => props.animateBy, () => props.direction], () => {
|
|
||||||
animationKey.value++;
|
|
||||||
completionFired.value = false;
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref, watchEffect, onUnmounted } from 'vue';
|
import { animate, Motion, MotionValue, useMotionValue } from 'motion-v';
|
||||||
import { Motion } from 'motion-v';
|
import { computed, onMounted, watch } from 'vue';
|
||||||
|
|
||||||
interface CircularTextProps {
|
interface CircularTextProps {
|
||||||
text: string;
|
text: string;
|
||||||
@@ -10,87 +10,59 @@ interface CircularTextProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<CircularTextProps>(), {
|
const props = withDefaults(defineProps<CircularTextProps>(), {
|
||||||
text: '',
|
|
||||||
spinDuration: 20,
|
spinDuration: 20,
|
||||||
onHover: 'speedUp',
|
onHover: 'speedUp',
|
||||||
className: ''
|
className: ''
|
||||||
});
|
});
|
||||||
|
|
||||||
const letters = computed(() => Array.from(props.text));
|
const letters = computed(() => Array.from(props.text));
|
||||||
const isHovered = ref(false);
|
const rotation: MotionValue<number> = useMotionValue(0);
|
||||||
|
|
||||||
const currentRotation = ref(0);
|
let currentAnimation: ReturnType<typeof animate> | null = null;
|
||||||
const animationId = ref<number | null>(null);
|
|
||||||
const lastTime = ref<number>(Date.now());
|
|
||||||
const rotationSpeed = ref<number>(0);
|
|
||||||
|
|
||||||
const getCurrentSpeed = () => {
|
const startRotation = (duration: number) => {
|
||||||
if (isHovered.value && props.onHover === 'pause') return 0;
|
currentAnimation?.stop();
|
||||||
|
const start = rotation.get();
|
||||||
|
|
||||||
const baseDuration = props.spinDuration;
|
currentAnimation = animate(rotation, start + 360, {
|
||||||
const baseSpeed = 360 / baseDuration;
|
duration,
|
||||||
|
ease: 'linear',
|
||||||
|
repeat: Infinity
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
if (!isHovered.value) return baseSpeed;
|
onMounted(() => {
|
||||||
|
startRotation(props.spinDuration);
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [props.spinDuration, props.text],
|
||||||
|
() => {
|
||||||
|
startRotation(props.spinDuration);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleHoverStart = () => {
|
||||||
|
if (!props.onHover) return;
|
||||||
|
|
||||||
switch (props.onHover) {
|
switch (props.onHover) {
|
||||||
case 'slowDown':
|
case 'slowDown':
|
||||||
return baseSpeed / 2;
|
startRotation(props.spinDuration * 2);
|
||||||
|
break;
|
||||||
case 'speedUp':
|
case 'speedUp':
|
||||||
return baseSpeed * 4;
|
startRotation(props.spinDuration / 4);
|
||||||
|
break;
|
||||||
|
case 'pause':
|
||||||
|
currentAnimation?.stop();
|
||||||
|
break;
|
||||||
case 'goBonkers':
|
case 'goBonkers':
|
||||||
return baseSpeed * 20;
|
startRotation(props.spinDuration / 20);
|
||||||
default:
|
break;
|
||||||
return baseSpeed;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getCurrentScale = () => {
|
|
||||||
return isHovered.value && props.onHover === 'goBonkers' ? 0.8 : 1;
|
|
||||||
};
|
|
||||||
|
|
||||||
const animate = () => {
|
|
||||||
const now = Date.now();
|
|
||||||
const deltaTime = (now - lastTime.value) / 1000;
|
|
||||||
lastTime.value = now;
|
|
||||||
|
|
||||||
const targetSpeed = getCurrentSpeed();
|
|
||||||
|
|
||||||
const speedDiff = targetSpeed - rotationSpeed.value;
|
|
||||||
const smoothingFactor = Math.min(1, deltaTime * 5);
|
|
||||||
rotationSpeed.value += speedDiff * smoothingFactor;
|
|
||||||
|
|
||||||
currentRotation.value = (currentRotation.value + rotationSpeed.value * deltaTime) % 360;
|
|
||||||
|
|
||||||
animationId.value = requestAnimationFrame(animate);
|
|
||||||
};
|
|
||||||
|
|
||||||
const startAnimation = () => {
|
|
||||||
if (animationId.value) {
|
|
||||||
cancelAnimationFrame(animationId.value);
|
|
||||||
}
|
|
||||||
lastTime.value = Date.now();
|
|
||||||
rotationSpeed.value = getCurrentSpeed();
|
|
||||||
animate();
|
|
||||||
};
|
|
||||||
|
|
||||||
watchEffect(() => {
|
|
||||||
startAnimation();
|
|
||||||
});
|
|
||||||
|
|
||||||
startAnimation();
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
if (animationId.value) {
|
|
||||||
cancelAnimationFrame(animationId.value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleHoverStart = () => {
|
|
||||||
isHovered.value = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleHoverEnd = () => {
|
const handleHoverEnd = () => {
|
||||||
isHovered.value = false;
|
startRotation(props.spinDuration);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getLetterTransform = (index: number) => {
|
const getLetterTransform = (index: number) => {
|
||||||
@@ -104,28 +76,24 @@ const getLetterTransform = (index: number) => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Motion
|
<Motion
|
||||||
:animate="{
|
tag="div"
|
||||||
rotate: currentRotation,
|
:class="[
|
||||||
scale: getCurrentScale()
|
'm-0 mx-auto rounded-full w-[200px] h-[200px] relative font-black text-white text-center cursor-pointer origin-center',
|
||||||
|
className
|
||||||
|
]"
|
||||||
|
:style="{
|
||||||
|
rotate: rotation
|
||||||
}"
|
}"
|
||||||
:transition="{
|
:initial="{
|
||||||
rotate: {
|
rotate: 0
|
||||||
duration: 0
|
|
||||||
},
|
|
||||||
scale: {
|
|
||||||
type: 'spring',
|
|
||||||
damping: 20,
|
|
||||||
stiffness: 300
|
|
||||||
}
|
|
||||||
}"
|
}"
|
||||||
:class="`m-0 mx-auto rounded-full w-[200px] h-[200px] relative font-black text-white text-center cursor-pointer origin-center ${props.className}`"
|
|
||||||
@mouseenter="handleHoverStart"
|
@mouseenter="handleHoverStart"
|
||||||
@mouseleave="handleHoverEnd"
|
@mouseleave="handleHoverEnd"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
v-for="(letter, i) in letters"
|
v-for="(letter, i) in letters"
|
||||||
:key="i"
|
:key="i"
|
||||||
class="absolute inline-block inset-0 text-2xl transition-all duration-500 ease-[cubic-bezier(0,0,0,1)]"
|
class="inline-block absolute inset-0 text-2xl transition-all duration-500 ease-[cubic-bezier(0,0,0,1)]"
|
||||||
:style="{
|
:style="{
|
||||||
transform: getLetterTransform(i),
|
transform: getLetterTransform(i),
|
||||||
WebkitTransform: getLetterTransform(i)
|
WebkitTransform: getLetterTransform(i)
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, onUnmounted, watch, nextTick, useTemplateRef } from 'vue';
|
import { computed, onBeforeUnmount, onMounted, useSlots, useTemplateRef, watch } from 'vue';
|
||||||
|
|
||||||
interface FuzzyTextProps {
|
interface FuzzyTextProps {
|
||||||
text: string;
|
|
||||||
fontSize?: number | string;
|
fontSize?: number | string;
|
||||||
fontWeight?: string | number;
|
fontWeight?: string | number;
|
||||||
fontFamily?: string;
|
fontFamily?: string;
|
||||||
@@ -10,78 +9,52 @@ interface FuzzyTextProps {
|
|||||||
enableHover?: boolean;
|
enableHover?: boolean;
|
||||||
baseIntensity?: number;
|
baseIntensity?: number;
|
||||||
hoverIntensity?: number;
|
hoverIntensity?: number;
|
||||||
|
fuzzRange?: number;
|
||||||
|
fps?: number;
|
||||||
|
direction?: 'horizontal' | 'vertical' | 'both';
|
||||||
|
transitionDuration?: number;
|
||||||
|
clickEffect?: boolean;
|
||||||
|
glitchMode?: boolean;
|
||||||
|
glitchInterval?: number;
|
||||||
|
glitchDuration?: number;
|
||||||
|
gradient?: string[] | null;
|
||||||
|
letterSpacing?: number;
|
||||||
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<FuzzyTextProps>(), {
|
const props = withDefaults(defineProps<FuzzyTextProps>(), {
|
||||||
text: '',
|
|
||||||
fontSize: 'clamp(2rem, 8vw, 8rem)',
|
fontSize: 'clamp(2rem, 8vw, 8rem)',
|
||||||
fontWeight: 900,
|
fontWeight: 900,
|
||||||
fontFamily: 'inherit',
|
fontFamily: 'inherit',
|
||||||
color: '#fff',
|
color: '#fff',
|
||||||
enableHover: true,
|
enableHover: true,
|
||||||
baseIntensity: 0.18,
|
baseIntensity: 0.18,
|
||||||
hoverIntensity: 0.5
|
hoverIntensity: 0.5,
|
||||||
|
fuzzRange: 30,
|
||||||
|
fps: 60,
|
||||||
|
direction: 'horizontal',
|
||||||
|
transitionDuration: 0,
|
||||||
|
clickEffect: false,
|
||||||
|
glitchMode: false,
|
||||||
|
glitchInterval: 2000,
|
||||||
|
glitchDuration: 200,
|
||||||
|
gradient: null,
|
||||||
|
letterSpacing: 0,
|
||||||
|
className: ''
|
||||||
});
|
});
|
||||||
|
|
||||||
const canvasRef = useTemplateRef<HTMLCanvasElement>('canvasRef');
|
const canvasRef = useTemplateRef<HTMLCanvasElement & { cleanupFuzzyText?: () => void }>('canvasRef');
|
||||||
|
const slots = useSlots();
|
||||||
|
|
||||||
let animationFrameId: number;
|
let animationFrameId: number;
|
||||||
let isCancelled = false;
|
let glitchTimeoutId: ReturnType<typeof setTimeout>;
|
||||||
let cleanup: (() => void) | null = null;
|
let glitchEndTimeoutId: ReturnType<typeof setTimeout>;
|
||||||
|
let clickTimeoutId: ReturnType<typeof setTimeout>;
|
||||||
|
let cancelled = false;
|
||||||
|
|
||||||
const waitForFont = async (fontFamily: string, fontWeight: string | number, fontSize: string): Promise<boolean> => {
|
const text = computed(() => (slots.default?.() ?? []).map(v => v.children).join(''));
|
||||||
if (document.fonts?.check) {
|
|
||||||
const fontString = `${fontWeight} ${fontSize} ${fontFamily}`;
|
|
||||||
|
|
||||||
if (document.fonts.check(fontString)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await document.fonts.load(fontString);
|
|
||||||
return document.fonts.check(fontString);
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('Font loading failed:', error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Promise(resolve => {
|
|
||||||
const canvas = document.createElement('canvas');
|
|
||||||
const ctx = canvas.getContext('2d');
|
|
||||||
if (!ctx) {
|
|
||||||
resolve(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.font = `${fontWeight} ${fontSize} ${fontFamily}`;
|
|
||||||
const testWidth = ctx.measureText('M').width;
|
|
||||||
|
|
||||||
let attempts = 0;
|
|
||||||
const checkFont = () => {
|
|
||||||
ctx.font = `${fontWeight} ${fontSize} ${fontFamily}`;
|
|
||||||
const newWidth = ctx.measureText('M').width;
|
|
||||||
|
|
||||||
if (newWidth !== testWidth && newWidth > 0) {
|
|
||||||
resolve(true);
|
|
||||||
} else if (attempts < 20) {
|
|
||||||
attempts++;
|
|
||||||
setTimeout(checkFont, 50);
|
|
||||||
} else {
|
|
||||||
resolve(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
setTimeout(checkFont, 10);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const initCanvas = async () => {
|
|
||||||
if (document.fonts?.ready) {
|
|
||||||
await document.fonts.ready;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isCancelled) return;
|
|
||||||
|
|
||||||
|
const init = async () => {
|
||||||
const canvas = canvasRef.value;
|
const canvas = canvasRef.value;
|
||||||
if (!canvas) return;
|
if (!canvas) return;
|
||||||
|
|
||||||
@@ -92,184 +65,185 @@ const initCanvas = async () => {
|
|||||||
props.fontFamily === 'inherit' ? window.getComputedStyle(canvas).fontFamily || 'sans-serif' : props.fontFamily;
|
props.fontFamily === 'inherit' ? window.getComputedStyle(canvas).fontFamily || 'sans-serif' : props.fontFamily;
|
||||||
|
|
||||||
const fontSizeStr = typeof props.fontSize === 'number' ? `${props.fontSize}px` : props.fontSize;
|
const fontSizeStr = typeof props.fontSize === 'number' ? `${props.fontSize}px` : props.fontSize;
|
||||||
let numericFontSize: number;
|
|
||||||
|
|
||||||
|
const fontString = `${props.fontWeight} ${fontSizeStr} ${computedFontFamily}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await document.fonts.load(fontString);
|
||||||
|
} catch {
|
||||||
|
await document.fonts.ready;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cancelled) return;
|
||||||
|
|
||||||
|
let numericFontSize: number;
|
||||||
if (typeof props.fontSize === 'number') {
|
if (typeof props.fontSize === 'number') {
|
||||||
numericFontSize = props.fontSize;
|
numericFontSize = props.fontSize;
|
||||||
} else {
|
} else {
|
||||||
const temp = document.createElement('span');
|
const temp = document.createElement('span');
|
||||||
temp.style.fontSize = props.fontSize;
|
temp.style.fontSize = props.fontSize;
|
||||||
temp.style.fontFamily = computedFontFamily;
|
|
||||||
document.body.appendChild(temp);
|
document.body.appendChild(temp);
|
||||||
const computedSize = window.getComputedStyle(temp).fontSize;
|
numericFontSize = parseFloat(getComputedStyle(temp).fontSize);
|
||||||
numericFontSize = parseFloat(computedSize);
|
|
||||||
document.body.removeChild(temp);
|
document.body.removeChild(temp);
|
||||||
}
|
}
|
||||||
|
|
||||||
const fontLoaded = await waitForFont(computedFontFamily, props.fontWeight, fontSizeStr);
|
|
||||||
if (!fontLoaded) {
|
|
||||||
console.warn(`Font not loaded: ${computedFontFamily}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const text = props.text;
|
|
||||||
|
|
||||||
const offscreen = document.createElement('canvas');
|
const offscreen = document.createElement('canvas');
|
||||||
const offCtx = offscreen.getContext('2d');
|
const offCtx = offscreen.getContext('2d')!;
|
||||||
if (!offCtx) return;
|
|
||||||
|
|
||||||
const fontString = `${props.fontWeight} ${fontSizeStr} ${computedFontFamily}`;
|
|
||||||
offCtx.font = fontString;
|
offCtx.font = fontString;
|
||||||
|
offCtx.textBaseline = 'alphabetic';
|
||||||
|
|
||||||
const testMetrics = offCtx.measureText('M');
|
let totalWidth = 0;
|
||||||
if (testMetrics.width === 0) {
|
if (props.letterSpacing !== 0) {
|
||||||
setTimeout(() => {
|
for (const char of text.value) {
|
||||||
if (!isCancelled) {
|
totalWidth += offCtx.measureText(char).width + props.letterSpacing;
|
||||||
initCanvas();
|
|
||||||
}
|
}
|
||||||
}, 100);
|
totalWidth -= props.letterSpacing;
|
||||||
|
} else {
|
||||||
|
totalWidth = offCtx.measureText(text.value).width;
|
||||||
|
}
|
||||||
|
|
||||||
|
const metrics = offCtx.measureText(text.value);
|
||||||
|
const ascent = metrics.actualBoundingBoxAscent ?? numericFontSize;
|
||||||
|
const descent = metrics.actualBoundingBoxDescent ?? numericFontSize * 0.2;
|
||||||
|
const height = Math.ceil(ascent + descent);
|
||||||
|
|
||||||
|
offscreen.width = Math.ceil(totalWidth) + 20;
|
||||||
|
offscreen.height = height;
|
||||||
|
|
||||||
|
offCtx.font = fontString;
|
||||||
|
offCtx.textBaseline = 'alphabetic';
|
||||||
|
|
||||||
|
if (props.gradient && props.gradient.length >= 2) {
|
||||||
|
const grad = offCtx.createLinearGradient(0, 0, offscreen.width, 0);
|
||||||
|
props.gradient.forEach((c, i) => grad.addColorStop(i / (props.gradient!.length - 1), c));
|
||||||
|
offCtx.fillStyle = grad;
|
||||||
|
} else {
|
||||||
|
offCtx.fillStyle = props.color;
|
||||||
|
}
|
||||||
|
|
||||||
|
let x = 10;
|
||||||
|
for (const char of text.value) {
|
||||||
|
offCtx.fillText(char, x, ascent);
|
||||||
|
x += offCtx.measureText(char).width + props.letterSpacing;
|
||||||
|
}
|
||||||
|
|
||||||
|
const marginX = props.fuzzRange + 20;
|
||||||
|
const marginY = props.direction === 'vertical' || props.direction === 'both' ? props.fuzzRange + 10 : 0;
|
||||||
|
|
||||||
|
canvas.width = offscreen.width + marginX * 2;
|
||||||
|
canvas.height = offscreen.height + marginY * 2;
|
||||||
|
ctx.translate(marginX, marginY);
|
||||||
|
|
||||||
|
let isHovering = false;
|
||||||
|
let isClicking = false;
|
||||||
|
let isGlitching = false;
|
||||||
|
let currentIntensity = props.baseIntensity;
|
||||||
|
let targetIntensity = props.baseIntensity;
|
||||||
|
let lastFrameTime = 0;
|
||||||
|
const frameDuration = 1000 / props.fps;
|
||||||
|
|
||||||
|
const startGlitch = () => {
|
||||||
|
if (!props.glitchMode || cancelled) return;
|
||||||
|
glitchTimeoutId = setTimeout(() => {
|
||||||
|
isGlitching = true;
|
||||||
|
glitchEndTimeoutId = setTimeout(() => {
|
||||||
|
isGlitching = false;
|
||||||
|
startGlitch();
|
||||||
|
}, props.glitchDuration);
|
||||||
|
}, props.glitchInterval);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (props.glitchMode) startGlitch();
|
||||||
|
|
||||||
|
const run = (ts: number) => {
|
||||||
|
if (cancelled) return;
|
||||||
|
|
||||||
|
if (ts - lastFrameTime < frameDuration) {
|
||||||
|
animationFrameId = requestAnimationFrame(run);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
offCtx.textBaseline = 'alphabetic';
|
lastFrameTime = ts;
|
||||||
const metrics = offCtx.measureText(text);
|
ctx.clearRect(-marginX, -marginY, offscreen.width + marginX * 2, offscreen.height + marginY * 2);
|
||||||
|
|
||||||
const actualLeft = metrics.actualBoundingBoxLeft ?? 0;
|
targetIntensity = isClicking || isGlitching ? 1 : isHovering ? props.hoverIntensity : props.baseIntensity;
|
||||||
const actualRight = metrics.actualBoundingBoxRight ?? metrics.width;
|
|
||||||
const actualAscent = metrics.actualBoundingBoxAscent ?? numericFontSize;
|
|
||||||
const actualDescent = metrics.actualBoundingBoxDescent ?? numericFontSize * 0.2;
|
|
||||||
|
|
||||||
const textBoundingWidth = Math.ceil(actualLeft + actualRight);
|
if (props.transitionDuration > 0) {
|
||||||
const tightHeight = Math.ceil(actualAscent + actualDescent);
|
const step = 1 / (props.transitionDuration / frameDuration);
|
||||||
|
currentIntensity += Math.sign(targetIntensity - currentIntensity) * step;
|
||||||
const extraWidthBuffer = 10;
|
currentIntensity = Math.min(
|
||||||
const offscreenWidth = textBoundingWidth + extraWidthBuffer;
|
Math.max(currentIntensity, Math.min(targetIntensity, currentIntensity)),
|
||||||
|
Math.max(targetIntensity, currentIntensity)
|
||||||
offscreen.width = offscreenWidth;
|
);
|
||||||
offscreen.height = tightHeight;
|
} else {
|
||||||
|
currentIntensity = targetIntensity;
|
||||||
const xOffset = extraWidthBuffer / 2;
|
|
||||||
offCtx.font = `${props.fontWeight} ${fontSizeStr} ${computedFontFamily}`;
|
|
||||||
offCtx.textBaseline = 'alphabetic';
|
|
||||||
offCtx.fillStyle = props.color;
|
|
||||||
offCtx.fillText(text, xOffset - actualLeft, actualAscent);
|
|
||||||
|
|
||||||
const horizontalMargin = 50;
|
|
||||||
const verticalMargin = 0;
|
|
||||||
canvas.width = offscreenWidth + horizontalMargin * 2;
|
|
||||||
canvas.height = tightHeight + verticalMargin * 2;
|
|
||||||
ctx.translate(horizontalMargin, verticalMargin);
|
|
||||||
|
|
||||||
const interactiveLeft = horizontalMargin + xOffset;
|
|
||||||
const interactiveTop = verticalMargin;
|
|
||||||
const interactiveRight = interactiveLeft + textBoundingWidth;
|
|
||||||
const interactiveBottom = interactiveTop + tightHeight;
|
|
||||||
|
|
||||||
let isHovering = false;
|
|
||||||
const fuzzRange = 30;
|
|
||||||
|
|
||||||
const run = () => {
|
|
||||||
if (isCancelled) return;
|
|
||||||
ctx.clearRect(-fuzzRange, -fuzzRange, offscreenWidth + 2 * fuzzRange, tightHeight + 2 * fuzzRange);
|
|
||||||
const intensity = isHovering ? props.hoverIntensity : props.baseIntensity;
|
|
||||||
for (let j = 0; j < tightHeight; j++) {
|
|
||||||
const dx = Math.floor(intensity * (Math.random() - 0.5) * fuzzRange);
|
|
||||||
ctx.drawImage(offscreen, 0, j, offscreenWidth, 1, dx, j, offscreenWidth, 1);
|
|
||||||
}
|
}
|
||||||
animationFrameId = window.requestAnimationFrame(run);
|
|
||||||
|
for (let y = 0; y < offscreen.height; y++) {
|
||||||
|
const dx = props.direction !== 'vertical' ? (Math.random() - 0.5) * currentIntensity * props.fuzzRange : 0;
|
||||||
|
const dy =
|
||||||
|
props.direction !== 'horizontal' ? (Math.random() - 0.5) * currentIntensity * props.fuzzRange * 0.5 : 0;
|
||||||
|
|
||||||
|
ctx.drawImage(offscreen, 0, y, offscreen.width, 1, dx, y + dy, offscreen.width, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
animationFrameId = requestAnimationFrame(run);
|
||||||
};
|
};
|
||||||
|
|
||||||
run();
|
animationFrameId = requestAnimationFrame(run);
|
||||||
|
|
||||||
const isInsideTextArea = (x: number, y: number) =>
|
const rectCheck = (x: number, y: number) =>
|
||||||
x >= interactiveLeft && x <= interactiveRight && y >= interactiveTop && y <= interactiveBottom;
|
x >= marginX && x <= marginX + offscreen.width && y >= marginY && y <= marginY + offscreen.height;
|
||||||
|
|
||||||
const handleMouseMove = (e: MouseEvent) => {
|
const mouseMove = (e: MouseEvent) => {
|
||||||
if (!props.enableHover) return;
|
if (!props.enableHover) return;
|
||||||
const rect = canvas.getBoundingClientRect();
|
const rect = canvas.getBoundingClientRect();
|
||||||
const x = e.clientX - rect.left;
|
isHovering = rectCheck(e.clientX - rect.left, e.clientY - rect.top);
|
||||||
const y = e.clientY - rect.top;
|
|
||||||
isHovering = isInsideTextArea(x, y);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMouseLeave = () => {
|
const mouseLeave = () => (isHovering = false);
|
||||||
isHovering = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleTouchMove = (e: TouchEvent) => {
|
const click = () => {
|
||||||
if (!props.enableHover) return;
|
if (!props.clickEffect) return;
|
||||||
e.preventDefault();
|
isClicking = true;
|
||||||
const rect = canvas.getBoundingClientRect();
|
clearTimeout(clickTimeoutId);
|
||||||
const touch = e.touches[0];
|
clickTimeoutId = setTimeout(() => (isClicking = false), 150);
|
||||||
const x = touch.clientX - rect.left;
|
|
||||||
const y = touch.clientY - rect.top;
|
|
||||||
isHovering = isInsideTextArea(x, y);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleTouchEnd = () => {
|
|
||||||
isHovering = false;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (props.enableHover) {
|
if (props.enableHover) {
|
||||||
canvas.addEventListener('mousemove', handleMouseMove);
|
canvas.addEventListener('mousemove', mouseMove);
|
||||||
canvas.addEventListener('mouseleave', handleMouseLeave);
|
canvas.addEventListener('mouseleave', mouseLeave);
|
||||||
canvas.addEventListener('touchmove', handleTouchMove, { passive: false });
|
|
||||||
canvas.addEventListener('touchend', handleTouchEnd);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cleanup = () => {
|
if (props.clickEffect) {
|
||||||
window.cancelAnimationFrame(animationFrameId);
|
canvas.addEventListener('click', click);
|
||||||
if (props.enableHover) {
|
|
||||||
canvas.removeEventListener('mousemove', handleMouseMove);
|
|
||||||
canvas.removeEventListener('mouseleave', handleMouseLeave);
|
|
||||||
canvas.removeEventListener('touchmove', handleTouchMove);
|
|
||||||
canvas.removeEventListener('touchend', handleTouchEnd);
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
cancelled = true;
|
||||||
|
cancelAnimationFrame(animationFrameId);
|
||||||
|
clearTimeout(glitchTimeoutId);
|
||||||
|
clearTimeout(glitchEndTimeoutId);
|
||||||
|
clearTimeout(clickTimeoutId);
|
||||||
|
canvas.removeEventListener('mousemove', mouseMove);
|
||||||
|
canvas.removeEventListener('mouseleave', mouseLeave);
|
||||||
|
canvas.removeEventListener('click', click);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(init);
|
||||||
nextTick(() => {
|
|
||||||
initCanvas();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
isCancelled = true;
|
|
||||||
if (animationFrameId) {
|
|
||||||
window.cancelAnimationFrame(animationFrameId);
|
|
||||||
}
|
|
||||||
if (cleanup) {
|
|
||||||
cleanup();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
[
|
() => ({ ...props, text: text.value }),
|
||||||
() => props.text,
|
|
||||||
() => props.fontSize,
|
|
||||||
() => props.fontWeight,
|
|
||||||
() => props.fontFamily,
|
|
||||||
() => props.color,
|
|
||||||
() => props.enableHover,
|
|
||||||
() => props.baseIntensity,
|
|
||||||
() => props.hoverIntensity
|
|
||||||
],
|
|
||||||
() => {
|
() => {
|
||||||
isCancelled = true;
|
cancelled = true;
|
||||||
if (animationFrameId) {
|
cancelAnimationFrame(animationFrameId);
|
||||||
window.cancelAnimationFrame(animationFrameId);
|
cancelled = false;
|
||||||
}
|
init();
|
||||||
if (cleanup) {
|
|
||||||
cleanup();
|
|
||||||
}
|
|
||||||
isCancelled = false;
|
|
||||||
nextTick(() => {
|
|
||||||
initCanvas();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<canvas ref="canvasRef" />
|
<canvas ref="canvasRef" :class="className" />
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,75 +1,140 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue';
|
import { Motion, useAnimationFrame, useMotionValue, useTransform } from 'motion-v';
|
||||||
|
import { computed, ref, useSlots } from 'vue';
|
||||||
|
|
||||||
interface GradientTextProps {
|
interface GradientTextProps {
|
||||||
text: string;
|
|
||||||
className?: string;
|
className?: string;
|
||||||
colors?: string[];
|
colors?: string[];
|
||||||
animationSpeed?: number;
|
animationSpeed?: number;
|
||||||
showBorder?: boolean;
|
showBorder?: boolean;
|
||||||
|
direction?: 'horizontal' | 'vertical' | 'diagonal';
|
||||||
|
pauseOnHover?: boolean;
|
||||||
|
yoyo?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<GradientTextProps>(), {
|
const props = withDefaults(defineProps<GradientTextProps>(), {
|
||||||
text: '',
|
|
||||||
className: '',
|
className: '',
|
||||||
colors: () => ['#ffaa40', '#9c40ff', '#ffaa40'],
|
colors: () => ['#27FF64', '#27FF64', '#A0FFBC'],
|
||||||
animationSpeed: 8,
|
animationSpeed: 8,
|
||||||
showBorder: false
|
showBorder: false,
|
||||||
|
direction: 'horizontal',
|
||||||
|
pauseOnHover: false,
|
||||||
|
yoyo: true
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const slots = useSlots();
|
||||||
|
const text = computed(() => (slots.default?.() ?? []).map(v => v.children).join(''));
|
||||||
|
|
||||||
|
const isPaused = ref(false);
|
||||||
|
const progress = useMotionValue(0);
|
||||||
|
const elapsedRef = ref(0);
|
||||||
|
const lastTimeRef = ref<number | null>(null);
|
||||||
|
|
||||||
|
const animationDuration = props.animationSpeed * 1000;
|
||||||
|
|
||||||
|
useAnimationFrame(time => {
|
||||||
|
if (isPaused.value) {
|
||||||
|
lastTimeRef.value = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastTimeRef.value === null) {
|
||||||
|
lastTimeRef.value = time;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const deltaTime = time - lastTimeRef.value;
|
||||||
|
lastTimeRef.value = time;
|
||||||
|
elapsedRef.value += deltaTime;
|
||||||
|
|
||||||
|
if (props.yoyo) {
|
||||||
|
const fullCycle = animationDuration * 2;
|
||||||
|
const cycleTime = elapsedRef.value % fullCycle;
|
||||||
|
|
||||||
|
if (cycleTime < animationDuration) {
|
||||||
|
progress.set((cycleTime / animationDuration) * 100);
|
||||||
|
} else {
|
||||||
|
progress.set(100 - ((cycleTime - animationDuration) / animationDuration) * 100);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Continuously increase position for seamless looping
|
||||||
|
progress.set((elapsedRef.value / animationDuration) * 100);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const backgroundPosition = useTransform(progress, p => {
|
||||||
|
if (props.direction === 'horizontal') {
|
||||||
|
return `${p}% 50%`;
|
||||||
|
} else if (props.direction === 'vertical') {
|
||||||
|
return `50% ${p}%`;
|
||||||
|
} else {
|
||||||
|
// For diagonal, move only horizontally to avoid interference patterns
|
||||||
|
return `${p}% 50%`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleMouseEnter = () => {
|
||||||
|
if (props.pauseOnHover) isPaused.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseLeave = () => {
|
||||||
|
if (props.pauseOnHover) isPaused.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const gradientAngle = computed(() =>
|
||||||
|
props.direction === 'horizontal' ? 'to right' : props.direction === 'vertical' ? 'to bottom' : 'to bottom right'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Duplicate first color at the end for seamless looping
|
||||||
|
const gradientColors = computed(() => [...props.colors, props.colors[0]].join(', '));
|
||||||
|
|
||||||
const gradientStyle = computed(() => ({
|
const gradientStyle = computed(() => ({
|
||||||
backgroundImage: `linear-gradient(to right, ${props.colors.join(', ')})`,
|
backgroundImage: `linear-gradient(${gradientAngle.value}, ${gradientColors.value})`,
|
||||||
animationDuration: `${props.animationSpeed}s`,
|
backgroundSize:
|
||||||
backgroundSize: '300% 100%',
|
props.direction === 'horizontal' ? '300% 100%' : props.direction === 'vertical' ? '100% 300%' : '300% 300%',
|
||||||
'--animation-duration': `${props.animationSpeed}s`
|
backgroundRepeat: 'repeat'
|
||||||
}));
|
|
||||||
|
|
||||||
const borderStyle = computed(() => ({
|
|
||||||
...gradientStyle.value
|
|
||||||
}));
|
|
||||||
|
|
||||||
const textStyle = computed(() => ({
|
|
||||||
...gradientStyle.value,
|
|
||||||
backgroundClip: 'text',
|
|
||||||
WebkitBackgroundClip: 'text'
|
|
||||||
}));
|
}));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<Motion
|
||||||
:class="`relative mx-auto flex max-w-fit flex-row items-center justify-center rounded-[1.25rem] font-medium backdrop-blur transition-shadow duration-500 overflow-hidden cursor-pointer ${className}`"
|
tag="div"
|
||||||
|
:class="[
|
||||||
|
'relative mx-auto flex max-w-fit flex-row items-center justify-center rounded-[1.25rem] font-medium backdrop-blur transition-shadow duration-500 overflow-hidden cursor-pointer',
|
||||||
|
className,
|
||||||
|
showBorder && 'py-1 px-2'
|
||||||
|
]"
|
||||||
|
@mouseenter="handleMouseEnter"
|
||||||
|
@mouseleave="handleMouseLeave"
|
||||||
>
|
>
|
||||||
<div
|
<Motion
|
||||||
|
tag="div"
|
||||||
v-if="showBorder"
|
v-if="showBorder"
|
||||||
class="absolute inset-0 bg-cover z-0 pointer-events-none animate-gradient"
|
class="z-0 absolute inset-0 rounded-[1.25rem] pointer-events-none"
|
||||||
:style="borderStyle"
|
:style="{ ...gradientStyle, backgroundPosition }"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="absolute inset-0 bg-black rounded-[1.25rem] z-[-1]"
|
class="z-[-1] absolute bg-black rounded-[1.25rem]"
|
||||||
style="width: calc(100% - 2px); height: calc(100% - 2px); left: 50%; top: 50%; transform: translate(-50%, -50%)"
|
:style="{
|
||||||
|
width: 'calc(100% - 2px)',
|
||||||
|
height: 'calc(100% - 2px)',
|
||||||
|
left: '50%',
|
||||||
|
top: '50%',
|
||||||
|
transform: 'translate(-50%, -50%)'
|
||||||
|
}"
|
||||||
/>
|
/>
|
||||||
</div>
|
</Motion>
|
||||||
|
|
||||||
<div class="inline-block relative z-2 text-transparent bg-cover animate-gradient" :style="textStyle">
|
<Motion
|
||||||
|
tag="div"
|
||||||
|
class="inline-block z-2 relative bg-clip-text text-transparent"
|
||||||
|
:style="{
|
||||||
|
...gradientStyle,
|
||||||
|
backgroundPosition,
|
||||||
|
WebkitBackgroundClip: 'text'
|
||||||
|
}"
|
||||||
|
>
|
||||||
{{ text }}
|
{{ text }}
|
||||||
</div>
|
</Motion>
|
||||||
</div>
|
</Motion>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
@keyframes gradient {
|
|
||||||
0% {
|
|
||||||
background-position: 0% 50%;
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
background-position: 100% 50%;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
background-position: 0% 50%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.animate-gradient {
|
|
||||||
animation: gradient var(--animation-duration, 8s) linear infinite;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -1,49 +1,135 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue';
|
import { Motion, useAnimationFrame, useMotionValue, useTransform } from 'motion-v';
|
||||||
|
import { computed, ref, watch } from 'vue';
|
||||||
|
|
||||||
interface ShinyTextProps {
|
interface ShinyTextProps {
|
||||||
text: string;
|
text: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
speed?: number;
|
speed?: number;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
color?: string;
|
||||||
|
shineColor?: string;
|
||||||
|
spread?: number;
|
||||||
|
yoyo?: boolean;
|
||||||
|
pauseOnHover?: boolean;
|
||||||
|
direction?: 'left' | 'right';
|
||||||
|
delay?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<ShinyTextProps>(), {
|
const props = withDefaults(defineProps<ShinyTextProps>(), {
|
||||||
text: '',
|
|
||||||
disabled: false,
|
disabled: false,
|
||||||
speed: 5,
|
speed: 2,
|
||||||
className: ''
|
className: '',
|
||||||
|
color: '#b5b5b5',
|
||||||
|
shineColor: '#ffffff',
|
||||||
|
spread: 120,
|
||||||
|
yoyo: false,
|
||||||
|
pauseOnHover: false,
|
||||||
|
direction: 'left',
|
||||||
|
delay: 0
|
||||||
});
|
});
|
||||||
|
|
||||||
const animationDuration = computed(() => `${props.speed}s`);
|
const isPaused = ref(false);
|
||||||
|
const progress = useMotionValue(0);
|
||||||
|
const elapsedRef = ref(0);
|
||||||
|
const lastTimeRef = ref<number | null>(null);
|
||||||
|
const directionRef = ref(props.direction === 'left' ? 1 : -1);
|
||||||
|
|
||||||
|
const animationDuration = computed(() => props.speed * 1000);
|
||||||
|
const delayDuration = computed(() => props.delay * 1000);
|
||||||
|
|
||||||
|
useAnimationFrame(time => {
|
||||||
|
if (props.disabled || isPaused.value) {
|
||||||
|
lastTimeRef.value = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastTimeRef.value === null) {
|
||||||
|
lastTimeRef.value = time;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const deltaTime = time - lastTimeRef.value;
|
||||||
|
lastTimeRef.value = time;
|
||||||
|
|
||||||
|
elapsedRef.value += deltaTime;
|
||||||
|
|
||||||
|
// Animation goes from 0 to 100
|
||||||
|
if (props.yoyo) {
|
||||||
|
const cycleDuration = animationDuration.value + delayDuration.value;
|
||||||
|
const fullCycle = cycleDuration * 2;
|
||||||
|
const cycleTime = elapsedRef.value % fullCycle;
|
||||||
|
|
||||||
|
if (cycleTime < animationDuration.value) {
|
||||||
|
// Forward animation: 0 -> 100
|
||||||
|
const p = (cycleTime / animationDuration.value) * 100;
|
||||||
|
progress.set(directionRef.value === 1 ? p : 100 - p);
|
||||||
|
} else if (cycleTime < cycleDuration) {
|
||||||
|
// Delay at end
|
||||||
|
progress.set(directionRef.value === 1 ? 100 : 0);
|
||||||
|
} else if (cycleTime < cycleDuration + animationDuration.value) {
|
||||||
|
// Reverse animation: 100 -> 0
|
||||||
|
const reverseTime = cycleTime - cycleDuration;
|
||||||
|
const p = 100 - (reverseTime / animationDuration.value) * 100;
|
||||||
|
progress.set(directionRef.value === 1 ? p : 100 - p);
|
||||||
|
} else {
|
||||||
|
// Delay at start
|
||||||
|
progress.set(directionRef.value === 1 ? 0 : 100);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const cycleDuration = animationDuration.value + delayDuration.value;
|
||||||
|
const cycleTime = elapsedRef.value % cycleDuration;
|
||||||
|
|
||||||
|
if (cycleTime < animationDuration.value) {
|
||||||
|
// Animation phase: 0 -> 100
|
||||||
|
const p = (cycleTime / animationDuration.value) * 100;
|
||||||
|
progress.set(directionRef.value === 1 ? p : 100 - p);
|
||||||
|
} else {
|
||||||
|
// Delay phase - hold at end (shine off-screen)
|
||||||
|
progress.set(directionRef.value === 1 ? 100 : 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.direction,
|
||||||
|
() => {
|
||||||
|
directionRef.value = props.direction === 'left' ? 1 : -1;
|
||||||
|
elapsedRef.value = 0;
|
||||||
|
progress.set(0);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
immediate: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const backgroundPosition = useTransform(progress, p => `${150 - p * 2}% center`);
|
||||||
|
|
||||||
|
const handleMouseEnter = () => {
|
||||||
|
if (props.pauseOnHover) isPaused.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseLeave = () => {
|
||||||
|
if (props.pauseOnHover) isPaused.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const gradientStyle = computed(() => ({
|
||||||
|
backgroundImage: `linear-gradient(${props.spread}deg, ${props.color} 0%, ${props.color} 35%, ${props.shineColor} 50%, ${props.color} 65%, ${props.color} 100%)`,
|
||||||
|
backgroundSize: '200% auto',
|
||||||
|
WebkitBackgroundClip: 'text',
|
||||||
|
backgroundClip: 'text',
|
||||||
|
WebkitTextFillColor: 'transparent'
|
||||||
|
}));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<Motion
|
||||||
:class="`text-[#b5b5b5a4] bg-clip-text inline-block ${!props.disabled ? 'animate-shine' : ''} ${props.className}`"
|
tag="span"
|
||||||
:style="{
|
:class="['inline-block', className]"
|
||||||
backgroundImage:
|
:style="{ ...gradientStyle, backgroundPosition }"
|
||||||
'linear-gradient(120deg, rgba(255, 255, 255, 0) 40%, rgba(255, 255, 255, 0.8) 50%, rgba(255, 255, 255, 0) 60%)',
|
@mouseenter="handleMouseEnter"
|
||||||
backgroundSize: '200% 100%',
|
@mouseleave="handleMouseLeave"
|
||||||
WebkitBackgroundClip: 'text',
|
|
||||||
animationDuration: animationDuration
|
|
||||||
}"
|
|
||||||
>
|
>
|
||||||
{{ props.text }}
|
{{ text }}
|
||||||
</div>
|
</Motion>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
@keyframes shine {
|
|
||||||
0% {
|
|
||||||
background-position: 100%;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
background-position: -100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.animate-shine {
|
|
||||||
animation: shine 5s linear infinite;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -1,18 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<p
|
<component :is="tag" ref="elRef" :style="styles" :class="classes">
|
||||||
ref="textRef"
|
|
||||||
:class="`split-parent overflow-hidden inline-block whitespace-normal ${className}`"
|
|
||||||
:style="{
|
|
||||||
textAlign,
|
|
||||||
wordWrap: 'break-word'
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
{{ text }}
|
{{ text }}
|
||||||
</p>
|
</component>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, onUnmounted, watch, nextTick, useTemplateRef } from 'vue';
|
import { ref, onMounted, watch, type CSSProperties, onBeforeUnmount, computed } from 'vue';
|
||||||
import { gsap } from 'gsap';
|
import { gsap } from 'gsap';
|
||||||
import { ScrollTrigger } from 'gsap/ScrollTrigger';
|
import { ScrollTrigger } from 'gsap/ScrollTrigger';
|
||||||
import { SplitText as GSAPSplitText } from 'gsap/SplitText';
|
import { SplitText as GSAPSplitText } from 'gsap/SplitText';
|
||||||
@@ -25,25 +18,27 @@ export interface SplitTextProps {
|
|||||||
delay?: number;
|
delay?: number;
|
||||||
duration?: number;
|
duration?: number;
|
||||||
ease?: string | ((t: number) => number);
|
ease?: string | ((t: number) => number);
|
||||||
splitType?: 'chars' | 'words' | 'lines' | 'words, chars';
|
splitType?: 'chars' | 'words' | 'lines';
|
||||||
from?: gsap.TweenVars;
|
from?: gsap.TweenVars;
|
||||||
to?: gsap.TweenVars;
|
to?: gsap.TweenVars;
|
||||||
threshold?: number;
|
threshold?: number;
|
||||||
rootMargin?: string;
|
rootMargin?: string;
|
||||||
textAlign?: 'left' | 'center' | 'right' | 'justify';
|
tag?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'p' | 'span';
|
||||||
|
textAlign?: CSSProperties['textAlign'];
|
||||||
onLetterAnimationComplete?: () => void;
|
onLetterAnimationComplete?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<SplitTextProps>(), {
|
const props = withDefaults(defineProps<SplitTextProps>(), {
|
||||||
className: '',
|
className: '',
|
||||||
delay: 100,
|
delay: 50,
|
||||||
duration: 0.6,
|
duration: 1.25,
|
||||||
ease: 'power3.out',
|
ease: 'power3.out',
|
||||||
splitType: 'chars',
|
splitType: 'chars',
|
||||||
from: () => ({ opacity: 0, y: 40 }),
|
from: () => ({ opacity: 0, y: 40 }),
|
||||||
to: () => ({ opacity: 1, y: 0 }),
|
to: () => ({ opacity: 1, y: 0 }),
|
||||||
threshold: 0.1,
|
threshold: 0.1,
|
||||||
rootMargin: '-100px',
|
rootMargin: '-100px',
|
||||||
|
tag: 'p',
|
||||||
textAlign: 'center'
|
textAlign: 'center'
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -51,144 +46,134 @@ const emit = defineEmits<{
|
|||||||
'animation-complete': [];
|
'animation-complete': [];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const textRef = useTemplateRef<HTMLParagraphElement>('textRef');
|
const elRef = ref<HTMLElement | null>(null);
|
||||||
const animationCompletedRef = ref(false);
|
const fontsLoaded = ref(false);
|
||||||
const scrollTriggerRef = ref<ScrollTrigger | null>(null);
|
const animationCompleted = ref(false);
|
||||||
const timelineRef = ref<gsap.core.Timeline | null>(null);
|
|
||||||
const splitterRef = ref<GSAPSplitText | null>(null);
|
|
||||||
|
|
||||||
const initializeAnimation = async () => {
|
let splitInstance: GSAPSplitText | null = null;
|
||||||
if (typeof window === 'undefined' || !textRef.value || !props.text) return;
|
|
||||||
|
|
||||||
await nextTick();
|
onMounted(() => {
|
||||||
|
if (document.fonts.status === 'loaded') {
|
||||||
|
fontsLoaded.value = true;
|
||||||
|
} else {
|
||||||
|
document.fonts.ready.then(() => {
|
||||||
|
fontsLoaded.value = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const el = textRef.value;
|
const runAnimation = () => {
|
||||||
|
if (!elRef.value || !props.text || !fontsLoaded.value) return;
|
||||||
|
if (animationCompleted.value) return;
|
||||||
|
|
||||||
animationCompletedRef.value = false;
|
const el = elRef.value as HTMLElement & {
|
||||||
|
_rbsplitInstance?: GSAPSplitText;
|
||||||
|
};
|
||||||
|
|
||||||
const absoluteLines = props.splitType === 'lines';
|
// cleanup previous
|
||||||
if (absoluteLines) el.style.position = 'relative';
|
if (el._rbsplitInstance) {
|
||||||
|
|
||||||
let splitter: GSAPSplitText;
|
|
||||||
try {
|
try {
|
||||||
splitter = new GSAPSplitText(el, {
|
el._rbsplitInstance.revert();
|
||||||
type: props.splitType,
|
} catch {}
|
||||||
absolute: absoluteLines,
|
el._rbsplitInstance = undefined;
|
||||||
linesClass: 'split-line'
|
|
||||||
});
|
|
||||||
splitterRef.value = splitter;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to create SplitText:', error);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let targets: Element[];
|
|
||||||
switch (props.splitType) {
|
|
||||||
case 'lines':
|
|
||||||
targets = splitter.lines;
|
|
||||||
break;
|
|
||||||
case 'words':
|
|
||||||
targets = splitter.words;
|
|
||||||
break;
|
|
||||||
case 'chars':
|
|
||||||
targets = splitter.chars;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
targets = splitter.chars;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!targets || targets.length === 0) {
|
|
||||||
console.warn('No targets found for SplitText animation');
|
|
||||||
splitter.revert();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
targets.forEach(t => {
|
|
||||||
(t as HTMLElement).style.willChange = 'transform, opacity';
|
|
||||||
});
|
|
||||||
|
|
||||||
const startPct = (1 - props.threshold) * 100;
|
const startPct = (1 - props.threshold) * 100;
|
||||||
const marginMatch = /^(-?\d+(?:\.\d+)?)(px|em|rem|%)?$/.exec(props.rootMargin);
|
const marginMatch = /^(-?\d+(?:\.\d+)?)(px|em|rem|%)?$/.exec(props.rootMargin);
|
||||||
const marginValue = marginMatch ? parseFloat(marginMatch[1]) : 0;
|
const marginValue = marginMatch ? parseFloat(marginMatch[1]) : 0;
|
||||||
const marginUnit = marginMatch ? marginMatch[2] || 'px' : 'px';
|
const marginUnit = marginMatch?.[2] || 'px';
|
||||||
const sign = marginValue < 0 ? `-=${Math.abs(marginValue)}${marginUnit}` : `+=${marginValue}${marginUnit}`;
|
|
||||||
|
const sign =
|
||||||
|
marginValue === 0
|
||||||
|
? ''
|
||||||
|
: marginValue < 0
|
||||||
|
? `-=${Math.abs(marginValue)}${marginUnit}`
|
||||||
|
: `+=${marginValue}${marginUnit}`;
|
||||||
|
|
||||||
const start = `top ${startPct}%${sign}`;
|
const start = `top ${startPct}%${sign}`;
|
||||||
|
|
||||||
const tl = gsap.timeline({
|
let targets: Element[] = [];
|
||||||
scrollTrigger: {
|
|
||||||
trigger: el,
|
|
||||||
start,
|
|
||||||
toggleActions: 'play none none none',
|
|
||||||
once: true,
|
|
||||||
onToggle: self => {
|
|
||||||
scrollTriggerRef.value = self;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
smoothChildTiming: true,
|
|
||||||
onComplete: () => {
|
|
||||||
animationCompletedRef.value = true;
|
|
||||||
gsap.set(targets, {
|
|
||||||
...props.to,
|
|
||||||
clearProps: 'willChange',
|
|
||||||
immediateRender: true
|
|
||||||
});
|
|
||||||
props.onLetterAnimationComplete?.();
|
|
||||||
emit('animation-complete');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
timelineRef.value = tl;
|
const assignTargets = (self: GSAPSplitText) => {
|
||||||
|
if (props.splitType.includes('chars') && self.chars?.length) targets = self.chars;
|
||||||
|
if (!targets.length && props.splitType.includes('words') && self.words?.length) targets = self.words;
|
||||||
|
if (!targets.length && props.splitType.includes('lines') && self.lines?.length) targets = self.lines;
|
||||||
|
if (!targets.length) targets = self.chars || self.words || self.lines;
|
||||||
|
};
|
||||||
|
|
||||||
tl.set(targets, { ...props.from, immediateRender: false, force3D: true });
|
splitInstance = new GSAPSplitText(el, {
|
||||||
tl.to(targets, {
|
type: props.splitType,
|
||||||
|
smartWrap: true,
|
||||||
|
autoSplit: props.splitType === 'lines',
|
||||||
|
linesClass: 'split-line',
|
||||||
|
wordsClass: 'split-word',
|
||||||
|
charsClass: 'split-char',
|
||||||
|
reduceWhiteSpace: false,
|
||||||
|
onSplit(self) {
|
||||||
|
assignTargets(self);
|
||||||
|
|
||||||
|
return gsap.fromTo(
|
||||||
|
targets,
|
||||||
|
{ ...props.from },
|
||||||
|
{
|
||||||
...props.to,
|
...props.to,
|
||||||
duration: props.duration,
|
duration: props.duration,
|
||||||
ease: props.ease,
|
ease: props.ease,
|
||||||
stagger: props.delay / 1000,
|
stagger: props.delay / 1000,
|
||||||
|
scrollTrigger: {
|
||||||
|
trigger: el,
|
||||||
|
start,
|
||||||
|
once: true,
|
||||||
|
fastScrollEnd: true,
|
||||||
|
anticipatePin: 0.4
|
||||||
|
},
|
||||||
|
onComplete() {
|
||||||
|
animationCompleted.value = true;
|
||||||
|
props.onLetterAnimationComplete?.();
|
||||||
|
emit('animation-complete');
|
||||||
|
},
|
||||||
|
willChange: 'transform, opacity',
|
||||||
force3D: true
|
force3D: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
el._rbsplitInstance = splitInstance;
|
||||||
};
|
};
|
||||||
|
|
||||||
const cleanup = () => {
|
|
||||||
if (timelineRef.value) {
|
|
||||||
timelineRef.value.kill();
|
|
||||||
timelineRef.value = null;
|
|
||||||
}
|
|
||||||
if (scrollTriggerRef.value) {
|
|
||||||
scrollTriggerRef.value.kill();
|
|
||||||
scrollTriggerRef.value = null;
|
|
||||||
}
|
|
||||||
if (splitterRef.value) {
|
|
||||||
gsap.killTweensOf(textRef.value);
|
|
||||||
splitterRef.value.revert();
|
|
||||||
splitterRef.value = null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
initializeAnimation();
|
|
||||||
});
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
cleanup();
|
|
||||||
});
|
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
[
|
() => [
|
||||||
() => props.text,
|
props.text,
|
||||||
() => props.delay,
|
props.delay,
|
||||||
() => props.duration,
|
props.duration,
|
||||||
() => props.ease,
|
props.ease,
|
||||||
() => props.splitType,
|
props.splitType,
|
||||||
() => props.from,
|
JSON.stringify(props.from),
|
||||||
() => props.to,
|
JSON.stringify(props.to),
|
||||||
() => props.threshold,
|
props.threshold,
|
||||||
() => props.rootMargin,
|
props.rootMargin,
|
||||||
() => props.onLetterAnimationComplete
|
fontsLoaded.value
|
||||||
],
|
],
|
||||||
() => {
|
runAnimation,
|
||||||
cleanup();
|
{ deep: true }
|
||||||
initializeAnimation();
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
ScrollTrigger.getAll().forEach(st => {
|
||||||
|
if (st.trigger === elRef.value) st.kill();
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
splitInstance?.revert();
|
||||||
|
} catch {}
|
||||||
|
});
|
||||||
|
|
||||||
|
const styles = computed(() => ({
|
||||||
|
textAlign: props.textAlign,
|
||||||
|
wordWrap: 'break-word',
|
||||||
|
willChange: 'transform, opacity'
|
||||||
|
}));
|
||||||
|
|
||||||
|
const classes = computed(() => `split-parent overflow-hidden inline-block whitespace-normal ${props.className}`);
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -150,9 +150,9 @@ onUnmounted(() => {
|
|||||||
'--glow-color': glowColor
|
'--glow-color': glowColor
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<spanx
|
<span
|
||||||
class="top-[-10px] left-[-10px] absolute filter-[drop-shadow(0_0_4px_var(--border-color,#fff))] border-[3px] border-(--border-color,#fff) border-r-0 border-b-0 rounded-[3px] w-4 h-4 transition-none"
|
class="top-[-10px] left-[-10px] absolute filter-[drop-shadow(0_0_4px_var(--border-color,#fff))] border-[3px] border-(--border-color,#fff) border-r-0 border-b-0 rounded-[3px] w-4 h-4 transition-none"
|
||||||
></spanx>
|
></span>
|
||||||
|
|
||||||
<span
|
<span
|
||||||
class="top-[-10px] right-[-10px] absolute filter-[drop-shadow(0_0_4px_var(--border-color,#fff))] border-[3px] border-(--border-color,#fff) border-b-0 border-l-0 rounded-[3px] w-4 h-4 transition-none"
|
class="top-[-10px] right-[-10px] absolute filter-[drop-shadow(0_0_4px_var(--border-color,#fff))] border-[3px] border-(--border-color,#fff) border-b-0 border-l-0 rounded-[3px] w-4 h-4 transition-none"
|
||||||
|
|||||||
200
src/demo/Animations/OrbitImagesDemo.vue
Normal file
200
src/demo/Animations/OrbitImagesDemo.vue
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
<template>
|
||||||
|
<TabbedLayout>
|
||||||
|
<template #preview>
|
||||||
|
<div class="demo-container h-[400px] overflow-hidden">
|
||||||
|
<RefreshButton @click="forceRerender" />
|
||||||
|
<OrbitImages
|
||||||
|
:key="key"
|
||||||
|
:images="images"
|
||||||
|
:shape="shape"
|
||||||
|
:radius-x="radiusX"
|
||||||
|
:radius-y="radiusY"
|
||||||
|
:radius="radius"
|
||||||
|
:rotation="rotation"
|
||||||
|
:duration="duration"
|
||||||
|
:item-size="itemSize"
|
||||||
|
:direction="direction"
|
||||||
|
:fill="fill"
|
||||||
|
:show-path="showPath"
|
||||||
|
:paused="paused"
|
||||||
|
:responsive="true"
|
||||||
|
path-color="rgba(255,255,255,0.15)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Customize>
|
||||||
|
<PreviewSelect
|
||||||
|
title="Shape"
|
||||||
|
v-model="shape"
|
||||||
|
:options="shapeOptions"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PreviewSelect
|
||||||
|
title="Direction"
|
||||||
|
v-model="direction"
|
||||||
|
:options="directionOptions"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PreviewSlider
|
||||||
|
title="Radius X"
|
||||||
|
v-model="radiusX"
|
||||||
|
:min="50"
|
||||||
|
:max="600"
|
||||||
|
:step="10"
|
||||||
|
value-unit="px"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PreviewSlider
|
||||||
|
title="Radius Y"
|
||||||
|
v-model="radiusY"
|
||||||
|
:min="50"
|
||||||
|
:max="600"
|
||||||
|
:step="10"
|
||||||
|
value-unit="px"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PreviewSlider
|
||||||
|
title="Radius"
|
||||||
|
v-model="radius"
|
||||||
|
:min="50"
|
||||||
|
:max="600"
|
||||||
|
:step="10"
|
||||||
|
value-unit="px"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PreviewSlider
|
||||||
|
title="Rotation"
|
||||||
|
v-model="rotation"
|
||||||
|
:min="-180"
|
||||||
|
:max="180"
|
||||||
|
:step="1"
|
||||||
|
value-unit="°"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PreviewSlider
|
||||||
|
title="Duration"
|
||||||
|
v-model="duration"
|
||||||
|
:min="5"
|
||||||
|
:max="120"
|
||||||
|
:step="5"
|
||||||
|
value-unit="s"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PreviewSlider
|
||||||
|
title="Item Size"
|
||||||
|
v-model="itemSize"
|
||||||
|
:min="20"
|
||||||
|
:max="120"
|
||||||
|
:step="4"
|
||||||
|
value-unit="px"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PreviewSwitch title="Fill (Distribute Evenly)" v-model="fill" />
|
||||||
|
|
||||||
|
<PreviewSwitch title="Show Path" v-model="showPath" />
|
||||||
|
|
||||||
|
<PreviewSwitch title="Paused" v-model="paused" />
|
||||||
|
</Customize>
|
||||||
|
|
||||||
|
<PropTable :data="propData" />
|
||||||
|
|
||||||
|
<Dependencies :dependency-list="[]" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #code>
|
||||||
|
<CodeExample :code-object="orbitImages" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #cli>
|
||||||
|
<CliInstallation :command="orbitImages.cli" />
|
||||||
|
</template>
|
||||||
|
</TabbedLayout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch } from 'vue';
|
||||||
|
import TabbedLayout from '../../components/common/TabbedLayout.vue';
|
||||||
|
import PropTable from '../../components/common/PropTable.vue';
|
||||||
|
import Dependencies from '../../components/code/Dependencies.vue';
|
||||||
|
import CliInstallation from '../../components/code/CliInstallation.vue';
|
||||||
|
import CodeExample from '../../components/code/CodeExample.vue';
|
||||||
|
import Customize from '../../components/common/Customize.vue';
|
||||||
|
import PreviewSlider from '../../components/common/PreviewSlider.vue';
|
||||||
|
import PreviewSwitch from '../../components/common/PreviewSwitch.vue';
|
||||||
|
import PreviewSelect from '../../components/common/PreviewSelect.vue';
|
||||||
|
import RefreshButton from '../../components/common/RefreshButton.vue';
|
||||||
|
import OrbitImages from '../../content/Animations/OrbitImages/OrbitImages.vue';
|
||||||
|
import { orbitImages } from '@/constants/code/Animations/orbitImagesCode';
|
||||||
|
import { useForceRerender } from '@/composables/useForceRerender';
|
||||||
|
|
||||||
|
const { rerenderKey: key, forceRerender } = useForceRerender();
|
||||||
|
|
||||||
|
const images = [
|
||||||
|
'https://picsum.photos/300/300?grayscale&random=1',
|
||||||
|
'https://picsum.photos/300/300?grayscale&random=2',
|
||||||
|
'https://picsum.photos/300/300?grayscale&random=3',
|
||||||
|
'https://picsum.photos/300/300?grayscale&random=4',
|
||||||
|
'https://picsum.photos/300/300?grayscale&random=5',
|
||||||
|
'https://picsum.photos/300/300?grayscale&random=6'
|
||||||
|
];
|
||||||
|
|
||||||
|
const shape = ref<string>('ellipse');
|
||||||
|
const radiusX = ref(340);
|
||||||
|
const radiusY = ref(80);
|
||||||
|
const radius = ref(160);
|
||||||
|
const rotation = ref(-8);
|
||||||
|
const duration = ref(30);
|
||||||
|
const itemSize = ref(80);
|
||||||
|
const direction = ref<'normal' | 'reverse'>('normal');
|
||||||
|
const fill = ref(true);
|
||||||
|
const showPath = ref(true);
|
||||||
|
const paused = ref(false);
|
||||||
|
|
||||||
|
const shapeOptions = [
|
||||||
|
{ label: 'Ellipse', value: 'ellipse' },
|
||||||
|
{ label: 'Circle', value: 'circle' },
|
||||||
|
{ label: 'Square', value: 'square' },
|
||||||
|
{ label: 'Rectangle', value: 'rectangle' },
|
||||||
|
{ label: 'Triangle', value: 'triangle' },
|
||||||
|
{ label: 'Star', value: 'star' },
|
||||||
|
{ label: 'Heart', value: 'heart' },
|
||||||
|
{ label: 'Infinity', value: 'infinity' },
|
||||||
|
{ label: 'Wave', value: 'wave' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const directionOptions = [
|
||||||
|
{ label: 'Normal', value: 'normal' },
|
||||||
|
{ label: 'Reverse', value: 'reverse' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const propData = [
|
||||||
|
{ name: 'images', type: 'string[]', default: '[]', description: 'Array of image URLs to orbit along the path.' },
|
||||||
|
{ name: 'altPrefix', type: 'string', default: '"Orbiting image"', description: 'Prefix for auto-generated alt attributes.' },
|
||||||
|
{ name: 'shape', type: 'OrbitShape', default: '"ellipse"', description: 'Preset shape: ellipse, circle, square, rectangle, triangle, star, heart, infinity, wave, or custom.' },
|
||||||
|
{ name: 'customPath', type: 'string', default: 'undefined', description: 'Custom SVG path string (used when shape="custom").' },
|
||||||
|
{ name: 'baseWidth', type: 'number', default: '1400', description: 'Base width for the design coordinate space used for responsive scaling.' },
|
||||||
|
{ name: 'radiusX', type: 'number', default: '700', description: 'Horizontal radius for ellipse/rectangle shapes.' },
|
||||||
|
{ name: 'radiusY', type: 'number', default: '170', description: 'Vertical radius for ellipse/rectangle shapes.' },
|
||||||
|
{ name: 'radius', type: 'number', default: '300', description: 'Radius for circle, square, triangle, star, heart shapes.' },
|
||||||
|
{ name: 'starPoints', type: 'number', default: '5', description: 'Number of points for star shape.' },
|
||||||
|
{ name: 'starInnerRatio', type: 'number', default: '0.5', description: 'Inner radius ratio for star (0-1).' },
|
||||||
|
{ name: 'rotation', type: 'number', default: '-8', description: 'Rotation angle of the entire orbit path in degrees.' },
|
||||||
|
{ name: 'duration', type: 'number', default: '40', description: 'Duration of one complete orbit in seconds.' },
|
||||||
|
{ name: 'itemSize', type: 'number', default: '64', description: 'Width/height of each orbiting item in pixels.' },
|
||||||
|
{ name: 'direction', type: '"normal" | "reverse"', default: '"normal"', description: 'Animation direction.' },
|
||||||
|
{ name: 'fill', type: 'boolean', default: 'true', description: 'Whether to distribute items evenly around the orbit.' },
|
||||||
|
{ name: 'width', type: 'number | "100%"', default: '100', description: 'Container width in pixels or "100%".' },
|
||||||
|
{ name: 'height', type: 'number | "auto"', default: '100', description: 'Container height in pixels or "auto".' },
|
||||||
|
{ name: 'className', type: 'string', default: '""', description: 'Additional CSS class for the container.' },
|
||||||
|
{ name: 'showPath', type: 'boolean', default: 'false', description: 'Whether to show the orbit path for debugging.' },
|
||||||
|
{ name: 'pathColor', type: 'string', default: '"rgba(0,0,0,0.1)"', description: 'Stroke color when showPath is true.' },
|
||||||
|
{ name: 'pathWidth', type: 'number', default: '2', description: 'Stroke width when showPath is true.' },
|
||||||
|
{ name: 'easing', type: 'string', default: '"linear"', description: 'Animation easing: linear, easeIn, easeOut, easeInOut.' },
|
||||||
|
{ name: 'paused', type: 'boolean', default: 'false', description: 'Whether the animation is paused.' },
|
||||||
|
{ name: 'responsive', type: 'boolean', default: 'false', description: 'Enable responsive scaling based on container width.' }
|
||||||
|
];
|
||||||
|
|
||||||
|
watch([shape, direction], () => {
|
||||||
|
forceRerender();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
259
src/demo/Backgrounds/GrainientDemo.vue
Normal file
259
src/demo/Backgrounds/GrainientDemo.vue
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
<template>
|
||||||
|
<TabbedLayout>
|
||||||
|
<template #preview>
|
||||||
|
<div class="relative p-0 h-[600px] overflow-hidden demo-container">
|
||||||
|
<Grainient
|
||||||
|
:key="rerenderKey"
|
||||||
|
:color1="color1"
|
||||||
|
:color2="color2"
|
||||||
|
:color3="color3"
|
||||||
|
:time-speed="timeSpeed"
|
||||||
|
:color-balance="colorBalance"
|
||||||
|
:warp-strength="warpStrength"
|
||||||
|
:warp-frequency="warpFrequency"
|
||||||
|
:warp-speed="warpSpeed"
|
||||||
|
:warp-amplitude="warpAmplitude"
|
||||||
|
:blend-angle="blendAngle"
|
||||||
|
:blend-softness="blendSoftness"
|
||||||
|
:rotation-amount="rotationAmount"
|
||||||
|
:noise-scale="noiseScale"
|
||||||
|
:grain-amount="grainAmount"
|
||||||
|
:grain-scale="grainScale"
|
||||||
|
:grain-animated="grainAnimated"
|
||||||
|
:contrast="contrast"
|
||||||
|
:gamma="gamma"
|
||||||
|
:saturation="saturation"
|
||||||
|
:centerX="centerX"
|
||||||
|
:centerY="centerY"
|
||||||
|
:zoom="zoom"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<BackgroundContent pill-text="New Background" headline="Grainy gradient colors with soft motion." />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Customize>
|
||||||
|
<div class="gap-4 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 mb-4">
|
||||||
|
<PreviewColor title="Color 1" v-model="color1" />
|
||||||
|
<PreviewColor title="Color 2" v-model="color2" />
|
||||||
|
<PreviewColor title="Color 3" v-model="color3" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="gap-4 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3">
|
||||||
|
<PreviewSlider title="Time Speed" :min="0" :max="5" :step="0.05" v-model="timeSpeed" />
|
||||||
|
<PreviewSlider title="Color Balance" :min="-1" :max="1" :step="0.01" v-model="colorBalance" />
|
||||||
|
<PreviewSlider title="Warp Strength" :min="0" :max="4" :step="0.05" v-model="warpStrength" />
|
||||||
|
<PreviewSlider title="Warp Frequency" :min="0" :max="12" :step="0.1" v-model="warpFrequency" />
|
||||||
|
<PreviewSlider title="Warp Speed" :min="0" :max="6" :step="0.1" v-model="warpSpeed" />
|
||||||
|
<PreviewSlider title="Warp Amplitude" :min="5" :max="80" :step="1" v-model="warpAmplitude" />
|
||||||
|
<PreviewSlider title="Blend Angle" :min="-180" :max="180" :step="1" v-model="blendAngle" />
|
||||||
|
<PreviewSlider title="Blend Softness" :min="0" :max="1" :step="0.01" v-model="blendSoftness" />
|
||||||
|
<PreviewSlider title="Rotation Amount" :min="0" :max="1440" :step="10" v-model="rotationAmount" />
|
||||||
|
<PreviewSlider title="Noise Scale" :min="0" :max="4" :step="0.05" v-model="noiseScale" />
|
||||||
|
<PreviewSlider title="Grain Amount" :min="0" :max="0.4" :step="0.01" v-model="grainAmount" />
|
||||||
|
<PreviewSlider title="Grain Scale" :min="0.2" :max="8" :step="0.1" v-model="grainScale" />
|
||||||
|
<PreviewSwitch title="Grain Animated" v-model="grainAnimated" />
|
||||||
|
<PreviewSlider title="Contrast" :min="0" :max="2.5" :step="0.05" v-model="contrast" />
|
||||||
|
<PreviewSlider title="Gamma" :min="0.4" :max="2.5" :step="0.05" v-model="gamma" />
|
||||||
|
<PreviewSlider title="Saturation" :min="0" :max="2.5" :step="0.05" v-model="saturation" />
|
||||||
|
<PreviewSlider title="Center Offset X" :min="-1" :max="1" :step="0.01" v-model="centerX" />
|
||||||
|
<PreviewSlider title="Center Offset Y" :min="-1" :max="1" :step="0.01" v-model="centerY" />
|
||||||
|
<PreviewSlider title="Zoom" :min="0.3" :max="3" :step="0.05" v-model="zoom" />
|
||||||
|
</div>
|
||||||
|
</Customize>
|
||||||
|
|
||||||
|
<PropTable :data="propData" />
|
||||||
|
<Dependencies :dependency-list="['ogl']" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #code>
|
||||||
|
<CodeExample :code-object="grainient" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #cli>
|
||||||
|
<CliInstallation :command="grainient.cli" />
|
||||||
|
</template>
|
||||||
|
</TabbedLayout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import CliInstallation from '@/components/code/CliInstallation.vue';
|
||||||
|
import CodeExample from '@/components/code/CodeExample.vue';
|
||||||
|
import Dependencies from '@/components/code/Dependencies.vue';
|
||||||
|
import BackgroundContent from '@/components/common/BackgroundContent.vue';
|
||||||
|
import Customize from '@/components/common/Customize.vue';
|
||||||
|
import PreviewColor from '@/components/common/PreviewColor.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 { useForceRerender } from '@/composables/useForceRerender';
|
||||||
|
import { grainient } from '@/constants/code/Backgrounds/grainientCode';
|
||||||
|
import Grainient from '@/content/Backgrounds/Grainient/Grainient.vue';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
const { rerenderKey } = useForceRerender();
|
||||||
|
|
||||||
|
const color1 = ref('#5227FF');
|
||||||
|
const color2 = ref('#FF9FFC');
|
||||||
|
const color3 = ref('#B19EEF');
|
||||||
|
const timeSpeed = ref(0.25);
|
||||||
|
const colorBalance = ref(0.0);
|
||||||
|
const warpStrength = ref(1.0);
|
||||||
|
const warpFrequency = ref(5.0);
|
||||||
|
const warpSpeed = ref(2.0);
|
||||||
|
const warpAmplitude = ref(50.0);
|
||||||
|
const blendAngle = ref(0.0);
|
||||||
|
const blendSoftness = ref(0.05);
|
||||||
|
const rotationAmount = ref(500.0);
|
||||||
|
const noiseScale = ref(2.0);
|
||||||
|
const grainAmount = ref(0.1);
|
||||||
|
const grainScale = ref(2.0);
|
||||||
|
const grainAnimated = ref(false);
|
||||||
|
const contrast = ref(1.5);
|
||||||
|
const gamma = ref(1.0);
|
||||||
|
const saturation = ref(1.0);
|
||||||
|
const centerX = ref(0.0);
|
||||||
|
const centerY = ref(0.0);
|
||||||
|
const zoom = ref(0.9);
|
||||||
|
|
||||||
|
const propData = [
|
||||||
|
{
|
||||||
|
name: 'color1',
|
||||||
|
type: 'string',
|
||||||
|
default: "'#48FF28'",
|
||||||
|
description: 'Primary light color used in the gradient blend.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'color2',
|
||||||
|
type: 'string',
|
||||||
|
default: "'#A2FFC6'",
|
||||||
|
description: 'Secondary accent color used in the gradient blend.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'color3',
|
||||||
|
type: 'string',
|
||||||
|
default: "'#9EF19E'",
|
||||||
|
description: 'Deep base color used in the gradient blend.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'timeSpeed',
|
||||||
|
type: 'number',
|
||||||
|
default: '0.25',
|
||||||
|
description: 'Animation speed multiplier for the gradient motion.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'colorBalance',
|
||||||
|
type: 'number',
|
||||||
|
default: '0.0',
|
||||||
|
description: 'Shifts the palette balance toward dark or lighter tones.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'warpStrength',
|
||||||
|
type: 'number',
|
||||||
|
default: '1.0',
|
||||||
|
description: 'Strength of the wave warp distortion (0 = none).'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'warpFrequency',
|
||||||
|
type: 'number',
|
||||||
|
default: '5.0',
|
||||||
|
description: 'Frequency of the wave warp.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'warpSpeed',
|
||||||
|
type: 'number',
|
||||||
|
default: '2.0',
|
||||||
|
description: 'Speed multiplier for the warp animation.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'warpAmplitude',
|
||||||
|
type: 'number',
|
||||||
|
default: '50.0',
|
||||||
|
description: 'Base amplitude for the warp distortion.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'blendAngle',
|
||||||
|
type: 'number',
|
||||||
|
default: '0.0',
|
||||||
|
description: 'Rotation angle for the color blend axis (degrees).'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'blendSoftness',
|
||||||
|
type: 'number',
|
||||||
|
default: '0.05',
|
||||||
|
description: 'Softens the blend edges between color layers.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'rotationAmount',
|
||||||
|
type: 'number',
|
||||||
|
default: '500.0',
|
||||||
|
description: 'Rotation amount driven by noise.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'noiseScale',
|
||||||
|
type: 'number',
|
||||||
|
default: '2.0',
|
||||||
|
description: 'Scales the noise frequency that drives rotation.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'grainAmount',
|
||||||
|
type: 'number',
|
||||||
|
default: '0.1',
|
||||||
|
description: 'Amount of film grain applied to the gradient.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'grainScale',
|
||||||
|
type: 'number',
|
||||||
|
default: '2.0',
|
||||||
|
description: 'Scale of the grain pattern.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'grainAnimated',
|
||||||
|
type: 'boolean',
|
||||||
|
default: 'false',
|
||||||
|
description: 'Animate grain over time.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'contrast',
|
||||||
|
type: 'number',
|
||||||
|
default: '1.5',
|
||||||
|
description: 'Overall contrast applied to the final color.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'gamma',
|
||||||
|
type: 'number',
|
||||||
|
default: '1.0',
|
||||||
|
description: 'Gamma correction for the final color.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'saturation',
|
||||||
|
type: 'number',
|
||||||
|
default: '1.0',
|
||||||
|
description: 'Saturation amount for the final color.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'centerX',
|
||||||
|
type: 'number',
|
||||||
|
default: '0.0',
|
||||||
|
description: 'Horizontal offset of the gradient center.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'centerY',
|
||||||
|
type: 'number',
|
||||||
|
default: '0.0',
|
||||||
|
description: 'Vertical offset of the gradient center.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'zoom',
|
||||||
|
type: 'number',
|
||||||
|
default: '0.9',
|
||||||
|
description: 'Zoom level for the gradient field.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'className',
|
||||||
|
type: 'string',
|
||||||
|
default: "''",
|
||||||
|
description: 'Additional CSS classes applied to the container.'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
</script>
|
||||||
@@ -1,33 +1,24 @@
|
|||||||
<template>
|
<template>
|
||||||
<TabbedLayout>
|
<TabbedLayout>
|
||||||
<template #preview>
|
<template #preview>
|
||||||
<div class="demo-container h-[400px] overflow-hidden">
|
<div class="h-[400px] overflow-hidden demo-container">
|
||||||
<RefreshButton @refresh="forceRerender" />
|
<RefreshButton @refresh="forceRerender" />
|
||||||
|
|
||||||
<BlurText
|
<BlurText
|
||||||
:key="rerenderKey"
|
:key="rerenderKey"
|
||||||
text="Isn't this so cool?!"
|
text="Isn't this so cool?!"
|
||||||
:delay="delay"
|
|
||||||
class-name="blur-text-demo"
|
|
||||||
:animate-by="animateBy"
|
:animate-by="animateBy"
|
||||||
:direction="direction"
|
:direction="direction"
|
||||||
:threshold="threshold"
|
:delay="delay"
|
||||||
:root-margin="rootMargin"
|
class-name="blur-text-demo"
|
||||||
:step-duration="stepDuration"
|
@animation-complete="showToast"
|
||||||
@animation-complete="
|
|
||||||
() => {
|
|
||||||
showCallback && showToast();
|
|
||||||
}
|
|
||||||
"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Customize>
|
<Customize>
|
||||||
<PreviewSwitch title="Show Completion Toast" v-model="showCallback" />
|
<div class="flex flex-wrap gap-4">
|
||||||
|
|
||||||
<div class="flex gap-4 flex-wrap">
|
|
||||||
<button
|
<button
|
||||||
class="text-xs bg-[#0b0b0b] rounded-[10px] border border-[#333] hover:bg-[#222] text-white h-8 px-3 transition-colors cursor-pointer"
|
class="bg-[#0b0b0b] hover:bg-[#222] px-3 border border-[#333] rounded-[10px] h-8 text-white text-xs transition-colors cursor-pointer"
|
||||||
@click="toggleAnimateBy"
|
@click="toggleAnimateBy"
|
||||||
>
|
>
|
||||||
Animate By:
|
Animate By:
|
||||||
@@ -35,7 +26,7 @@
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="text-xs bg-[#0b0b0b] rounded-[10px] border border-[#333] hover:bg-[#222] text-white h-8 px-3 transition-colors cursor-pointer"
|
class="bg-[#0b0b0b] hover:bg-[#222] px-3 border border-[#333] rounded-[10px] h-8 text-white text-xs transition-colors cursor-pointer"
|
||||||
@click="toggleDirection"
|
@click="toggleDirection"
|
||||||
>
|
>
|
||||||
Direction:
|
Direction:
|
||||||
@@ -43,11 +34,15 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<PreviewSlider title="Delay (ms)" v-model="delay" :min="50" :max="500" :step="10" />
|
<PreviewSlider
|
||||||
|
title="Delay"
|
||||||
<PreviewSlider title="Step Duration (s)" v-model="stepDuration" :min="0.1" :max="1" :step="0.05" />
|
v-model="delay"
|
||||||
|
:min="50"
|
||||||
<PreviewSlider title="Threshold" v-model="threshold" :min="0.1" :max="1" :step="0.1" />
|
:max="500"
|
||||||
|
:step="10"
|
||||||
|
value-unit="ms"
|
||||||
|
@update:model-value="forceRerender"
|
||||||
|
/>
|
||||||
</Customize>
|
</Customize>
|
||||||
|
|
||||||
<PropTable :data="propData" />
|
<PropTable :data="propData" />
|
||||||
@@ -66,30 +61,26 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue';
|
import CliInstallation from '@/components/code/CliInstallation.vue';
|
||||||
import TabbedLayout from '../../components/common/TabbedLayout.vue';
|
import CodeExample from '@/components/code/CodeExample.vue';
|
||||||
import RefreshButton from '../../components/common/RefreshButton.vue';
|
import Dependencies from '@/components/code/Dependencies.vue';
|
||||||
import PropTable from '../../components/common/PropTable.vue';
|
import Customize from '@/components/common/Customize.vue';
|
||||||
import Dependencies from '../../components/code/Dependencies.vue';
|
import PreviewSlider from '@/components/common/PreviewSlider.vue';
|
||||||
import CliInstallation from '../../components/code/CliInstallation.vue';
|
import PropTable from '@/components/common/PropTable.vue';
|
||||||
import CodeExample from '../../components/code/CodeExample.vue';
|
import RefreshButton from '@/components/common/RefreshButton.vue';
|
||||||
import Customize from '../../components/common/Customize.vue';
|
import TabbedLayout from '@/components/common/TabbedLayout.vue';
|
||||||
import PreviewSwitch from '../../components/common/PreviewSwitch.vue';
|
|
||||||
import PreviewSlider from '../../components/common/PreviewSlider.vue';
|
|
||||||
import BlurText from '../../content/TextAnimations/BlurText/BlurText.vue';
|
|
||||||
import { blurText } from '@/constants/code/TextAnimations/blurTextCode';
|
|
||||||
import { useToast } from 'primevue/usetoast';
|
|
||||||
import { useForceRerender } from '@/composables/useForceRerender';
|
import { useForceRerender } from '@/composables/useForceRerender';
|
||||||
|
import { blurText } from '@/constants/code/TextAnimations/blurTextCode';
|
||||||
|
import BlurText from '@/content/TextAnimations/BlurText/BlurText.vue';
|
||||||
|
import { useToast } from 'primevue/usetoast';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
const { rerenderKey, forceRerender } = useForceRerender();
|
||||||
|
const toast = useToast();
|
||||||
|
|
||||||
const animateBy = ref<'words' | 'letters'>('words');
|
const animateBy = ref<'words' | 'letters'>('words');
|
||||||
const direction = ref<'top' | 'bottom'>('top');
|
const direction = ref<'top' | 'bottom'>('top');
|
||||||
const delay = ref(200);
|
const delay = ref(200);
|
||||||
const stepDuration = ref(0.35);
|
|
||||||
const threshold = ref(0.1);
|
|
||||||
const rootMargin = ref('0px');
|
|
||||||
const showCallback = ref(true);
|
|
||||||
const toast = useToast();
|
|
||||||
const { rerenderKey, forceRerender } = useForceRerender();
|
|
||||||
|
|
||||||
const toggleAnimateBy = () => {
|
const toggleAnimateBy = () => {
|
||||||
animateBy.value = animateBy.value === 'words' ? 'letters' : 'words';
|
animateBy.value = animateBy.value === 'words' ? 'letters' : 'words';
|
||||||
@@ -110,18 +101,23 @@ const showToast = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const propData = [
|
const propData = [
|
||||||
{ name: 'text', type: 'string', default: '""', description: 'The text content to animate.' },
|
{
|
||||||
|
name: 'text',
|
||||||
|
type: 'string',
|
||||||
|
default: '""',
|
||||||
|
description: 'The text content to animate.'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'animateBy',
|
name: 'animateBy',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
default: '"words"',
|
default: '"words"',
|
||||||
description: 'Determines whether to animate by "words" or "letters".'
|
description: "Determines whether to animate by 'words' or 'letters'."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'direction',
|
name: 'direction',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
default: '"top"',
|
default: '"top"',
|
||||||
description: 'Direction from which the words/letters appear ("top" or "bottom").'
|
description: "Direction from which the words/letters appear ('top' or 'bottom')."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'delay',
|
name: 'delay',
|
||||||
@@ -141,16 +137,12 @@ const propData = [
|
|||||||
default: '0.1',
|
default: '0.1',
|
||||||
description: 'Intersection threshold for triggering the animation.'
|
description: 'Intersection threshold for triggering the animation.'
|
||||||
},
|
},
|
||||||
{ name: 'rootMargin', type: 'string', default: '"0px"', description: 'Root margin for the intersection observer.' },
|
|
||||||
{ name: 'className', type: 'string', default: '""', description: 'Additional class names to style the component.' },
|
|
||||||
{ name: 'animationFrom', type: 'object', default: 'undefined', description: 'Custom initial animation properties.' },
|
|
||||||
{
|
{
|
||||||
name: 'animationTo',
|
name: 'rootMargin',
|
||||||
type: 'array',
|
type: 'string',
|
||||||
default: 'undefined',
|
default: '"0px"',
|
||||||
description: 'Custom target animation properties array.'
|
description: 'Root margin for the intersection observer.'
|
||||||
},
|
},
|
||||||
{ name: 'easing', type: 'function', default: '(t) => t', description: 'Custom easing function for the animation.' },
|
|
||||||
{
|
{
|
||||||
name: 'onAnimationComplete',
|
name: 'onAnimationComplete',
|
||||||
type: 'function',
|
type: 'function',
|
||||||
|
|||||||
@@ -1,28 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<TabbedLayout>
|
<TabbedLayout>
|
||||||
<template #preview>
|
<template #preview>
|
||||||
<div class="demo-container h-[400px] overflow-hidden">
|
<div class="h-[400px] overflow-hidden demo-container">
|
||||||
<CircularText
|
<CircularText :key="rerenderKey" :text="text" :spin-duration="spinDuration" :on-hover="onHover" />
|
||||||
:key="rerenderKey"
|
|
||||||
text="VUE * BITS * IS * AWESOME * "
|
|
||||||
:spin-duration="spinDuration"
|
|
||||||
:on-hover="onHover"
|
|
||||||
class-name="text-blue-500"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Customize>
|
<Customize>
|
||||||
<div class="flex gap-4 flex-wrap">
|
<PreviewText title="Text" v-model="text" />
|
||||||
<button
|
<PreviewSelect title="On Hover" v-model="onHover" :options="hoverOptions" />
|
||||||
class="text-xs bg-[#0b0b0b] rounded-[10px] border border-[#1e3721] hover:bg-[#1e3721] text-white h-8 px-3 transition-colors"
|
<PreviewSlider title="Spin Duration (s)" v-model="spinDuration" :min="1" :max="60" :step="1" />
|
||||||
@click="toggleOnHover"
|
|
||||||
>
|
|
||||||
On Hover:
|
|
||||||
<span class="text-[#a1a1aa]"> {{ onHover }}</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<PreviewSlider title="Spin Duration (s)" v-model="spinDuration" :min="1" :max="50" :step="1" />
|
|
||||||
</Customize>
|
</Customize>
|
||||||
|
|
||||||
<PropTable :data="propData" />
|
<PropTable :data="propData" />
|
||||||
@@ -41,45 +27,58 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue';
|
import CliInstallation from '@/components/code/CliInstallation.vue';
|
||||||
import TabbedLayout from '../../components/common/TabbedLayout.vue';
|
import CodeExample from '@/components/code/CodeExample.vue';
|
||||||
import PropTable from '../../components/common/PropTable.vue';
|
import Dependencies from '@/components/code/Dependencies.vue';
|
||||||
import Dependencies from '../../components/code/Dependencies.vue';
|
import Customize from '@/components/common/Customize.vue';
|
||||||
import CliInstallation from '../../components/code/CliInstallation.vue';
|
import PreviewSelect from '@/components/common/PreviewSelect.vue';
|
||||||
import CodeExample from '../../components/code/CodeExample.vue';
|
import PreviewSlider from '@/components/common/PreviewSlider.vue';
|
||||||
import Customize from '../../components/common/Customize.vue';
|
import PreviewText from '@/components/common/PreviewText.vue';
|
||||||
import PreviewSlider from '../../components/common/PreviewSlider.vue';
|
import PropTable from '@/components/common/PropTable.vue';
|
||||||
import CircularText from '../../content/TextAnimations/CircularText/CircularText.vue';
|
import TabbedLayout from '@/components/common/TabbedLayout.vue';
|
||||||
import { circularText } from '@/constants/code/TextAnimations/circularTextCode';
|
|
||||||
import { useForceRerender } from '@/composables/useForceRerender';
|
import { useForceRerender } from '@/composables/useForceRerender';
|
||||||
|
import { circularText } from '@/constants/code/TextAnimations/circularTextCode';
|
||||||
|
import CircularText from '@/content/TextAnimations/CircularText/CircularText.vue';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
const { rerenderKey } = useForceRerender();
|
||||||
|
|
||||||
|
const text = ref('VUE*BITS*COMPONENTS*');
|
||||||
const onHover = ref<'slowDown' | 'speedUp' | 'pause' | 'goBonkers'>('speedUp');
|
const onHover = ref<'slowDown' | 'speedUp' | 'pause' | 'goBonkers'>('speedUp');
|
||||||
const spinDuration = ref(20);
|
const spinDuration = ref(20);
|
||||||
const { rerenderKey, forceRerender } = useForceRerender();
|
|
||||||
|
|
||||||
const hoverOptions: Array<'slowDown' | 'speedUp' | 'pause' | 'goBonkers'> = [
|
const hoverOptions = [
|
||||||
'slowDown',
|
{ label: 'Slow Down', value: 'slowDown' },
|
||||||
'speedUp',
|
{ label: 'Speed Up', value: 'speedUp' },
|
||||||
'pause',
|
{ label: 'Pause', value: 'pause' },
|
||||||
'goBonkers'
|
{ label: 'Go Bonkers', value: 'goBonkers' }
|
||||||
];
|
];
|
||||||
|
|
||||||
const toggleOnHover = () => {
|
|
||||||
const currentIndex = hoverOptions.indexOf(onHover.value);
|
|
||||||
const nextIndex = (currentIndex + 1) % hoverOptions.length;
|
|
||||||
onHover.value = hoverOptions[nextIndex];
|
|
||||||
forceRerender();
|
|
||||||
};
|
|
||||||
|
|
||||||
const propData = [
|
const propData = [
|
||||||
{ name: 'text', type: 'string', default: '""', description: 'The text content to display in a circular pattern.' },
|
{
|
||||||
{ name: 'spinDuration', type: 'number', default: '20', description: 'Duration of one full rotation in seconds.' },
|
name: 'text',
|
||||||
|
type: 'string',
|
||||||
|
default: "''",
|
||||||
|
description: 'The text to display in a circular layout.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'spinDuration',
|
||||||
|
type: 'number',
|
||||||
|
default: '20',
|
||||||
|
description: 'The duration (in seconds) for one full rotation.'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'onHover',
|
name: 'onHover',
|
||||||
type: 'string',
|
type: "'slowDown' | 'speedUp' | 'pause' | 'goBonkers'",
|
||||||
default: '"speedUp"',
|
default: 'undefined',
|
||||||
description: 'Hover behavior: "slowDown", "speedUp", "pause", or "goBonkers".'
|
description:
|
||||||
|
"Specifies the hover behavior variant. Options include 'slowDown', 'speedUp', 'pause', and 'goBonkers'."
|
||||||
},
|
},
|
||||||
{ name: 'className', type: 'string', default: '""', description: 'Additional class names to style the component.' }
|
{
|
||||||
|
name: 'className',
|
||||||
|
type: 'string',
|
||||||
|
default: "''",
|
||||||
|
description: 'Optional additional CSS classes to apply to the component.'
|
||||||
|
}
|
||||||
];
|
];
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,37 +1,74 @@
|
|||||||
<template>
|
<template>
|
||||||
<TabbedLayout>
|
<TabbedLayout>
|
||||||
<template #preview>
|
<template #preview>
|
||||||
<div class="demo-container h-[500px] overflow-hidden">
|
<div class="h-[500px] overflow-hidden demo-container">
|
||||||
<div class="flex flex-col items-center justify-center h-full">
|
<div class="flex flex-col justify-center items-center h-full">
|
||||||
<FuzzyText
|
<FuzzyText
|
||||||
:key="`main-${rerenderKey}`"
|
|
||||||
text="404"
|
|
||||||
:base-intensity="baseIntensity"
|
:base-intensity="baseIntensity"
|
||||||
:hover-intensity="hoverIntensity"
|
:hover-intensity="hoverIntensity"
|
||||||
:enable-hover="enableHover"
|
:enable-hover="enableHover"
|
||||||
|
:fuzz-range="fuzzRange"
|
||||||
|
:fps="fps"
|
||||||
|
:direction="direction"
|
||||||
|
:transition-duration="transitionDuration"
|
||||||
|
:click-effect="clickEffect"
|
||||||
|
:glitch-mode="glitchMode"
|
||||||
|
:glitch-interval="glitchInterval"
|
||||||
|
:glitch-duration="glitchDuration"
|
||||||
|
:letter-spacing="letterSpacing"
|
||||||
:font-size="140"
|
:font-size="140"
|
||||||
/>
|
>
|
||||||
|
404
|
||||||
|
</FuzzyText>
|
||||||
<div class="my-1" />
|
<div class="my-1" />
|
||||||
|
|
||||||
<FuzzyText
|
<FuzzyText
|
||||||
:key="`sub-${rerenderKey}`"
|
|
||||||
text="not found"
|
|
||||||
:base-intensity="baseIntensity"
|
:base-intensity="baseIntensity"
|
||||||
:hover-intensity="hoverIntensity"
|
:hover-intensity="hoverIntensity"
|
||||||
:enable-hover="enableHover"
|
:enable-hover="enableHover"
|
||||||
|
:fuzz-range="fuzzRange"
|
||||||
|
:fps="fps"
|
||||||
|
:direction="direction"
|
||||||
|
:transition-duration="transitionDuration"
|
||||||
|
:click-effect="clickEffect"
|
||||||
|
:glitch-mode="glitchMode"
|
||||||
|
:glitch-interval="glitchInterval"
|
||||||
|
:glitch-duration="glitchDuration"
|
||||||
|
:letter-spacing="letterSpacing"
|
||||||
:font-size="70"
|
:font-size="70"
|
||||||
font-family="Gochi Hand"
|
font-family="Gochi Hand"
|
||||||
/>
|
>
|
||||||
|
not found
|
||||||
|
</FuzzyText>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Customize>
|
<Customize>
|
||||||
<PreviewSlider title="Base Intensity" v-model="baseIntensity" :min="0" :max="1" :step="0.01" />
|
<PreviewSlider title="Base Intensity" v-model="baseIntensity" :min="0" :max="1" :step="0.01" />
|
||||||
|
|
||||||
<PreviewSlider title="Hover Intensity" v-model="hoverIntensity" :min="0" :max="2" :step="0.01" />
|
<PreviewSlider title="Hover Intensity" v-model="hoverIntensity" :min="0" :max="2" :step="0.01" />
|
||||||
|
<PreviewSlider title="Fuzz Range" v-model="fuzzRange" :min="5" :max="100" :step="1" />
|
||||||
|
<PreviewSlider title="FPS" v-model="fps" :min="10" :max="120" :step="5" />
|
||||||
|
<PreviewSlider title="Transition Duration" v-model="transitionDuration" :min="0" :max="60" :step="1" />
|
||||||
|
<PreviewSlider title="Letter Spacing" v-model="letterSpacing" :min="-10" :max="50" :step="1" />
|
||||||
|
<PreviewSelect title="Direction" v-model="direction" :options="directionOptions" />
|
||||||
<PreviewSwitch title="Enable Hover" v-model="enableHover" />
|
<PreviewSwitch title="Enable Hover" v-model="enableHover" />
|
||||||
|
<PreviewSwitch title="Click Effect" v-model="clickEffect" />
|
||||||
|
<PreviewSwitch title="Glitch Mode" v-model="glitchMode" />
|
||||||
|
<PreviewSlider
|
||||||
|
title="Glitch Interval"
|
||||||
|
v-model="glitchInterval"
|
||||||
|
:min="500"
|
||||||
|
:max="5000"
|
||||||
|
:step="100"
|
||||||
|
:disabled="!glitchMode"
|
||||||
|
/>
|
||||||
|
<PreviewSlider
|
||||||
|
title="Glitch Duration"
|
||||||
|
v-model="glitchDuration"
|
||||||
|
:min="50"
|
||||||
|
:max="1000"
|
||||||
|
:step="50"
|
||||||
|
:disabled="!glitchMode"
|
||||||
|
/>
|
||||||
</Customize>
|
</Customize>
|
||||||
|
|
||||||
<PropTable :data="propData" />
|
<PropTable :data="propData" />
|
||||||
@@ -48,35 +85,47 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue';
|
import CliInstallation from '@/components/code/CliInstallation.vue';
|
||||||
import TabbedLayout from '../../components/common/TabbedLayout.vue';
|
import CodeExample from '@/components/code/CodeExample.vue';
|
||||||
import PropTable from '../../components/common/PropTable.vue';
|
import Customize from '@/components/common/Customize.vue';
|
||||||
import CliInstallation from '../../components/code/CliInstallation.vue';
|
import PreviewSlider from '@/components/common/PreviewSlider.vue';
|
||||||
import CodeExample from '../../components/code/CodeExample.vue';
|
import PreviewSwitch from '@/components/common/PreviewSwitch.vue';
|
||||||
import Customize from '../../components/common/Customize.vue';
|
import PropTable from '@/components/common/PropTable.vue';
|
||||||
import PreviewSlider from '../../components/common/PreviewSlider.vue';
|
import TabbedLayout from '@/components/common/TabbedLayout.vue';
|
||||||
import PreviewSwitch from '../../components/common/PreviewSwitch.vue';
|
|
||||||
import FuzzyText from '../../content/TextAnimations/FuzzyText/FuzzyText.vue';
|
|
||||||
import { fuzzyText } from '@/constants/code/TextAnimations/fuzzyTextCode';
|
import { fuzzyText } from '@/constants/code/TextAnimations/fuzzyTextCode';
|
||||||
import { useForceRerender } from '@/composables/useForceRerender';
|
import FuzzyText from '@/content/TextAnimations/FuzzyText/FuzzyText.vue';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
const baseIntensity = ref(0.2);
|
const baseIntensity = ref(0.2);
|
||||||
const hoverIntensity = ref(0.5);
|
const hoverIntensity = ref(0.5);
|
||||||
const enableHover = ref(true);
|
const enableHover = ref(true);
|
||||||
|
const fuzzRange = ref(30);
|
||||||
|
const fps = ref(60);
|
||||||
|
const direction = ref<'horizontal' | 'vertical' | 'both'>('horizontal');
|
||||||
|
const transitionDuration = ref(0);
|
||||||
|
const clickEffect = ref(false);
|
||||||
|
const glitchMode = ref(false);
|
||||||
|
const glitchInterval = ref(2000);
|
||||||
|
const glitchDuration = ref(200);
|
||||||
|
const letterSpacing = ref(0);
|
||||||
|
|
||||||
const { rerenderKey } = useForceRerender();
|
const directionOptions = [
|
||||||
|
{ value: 'horizontal', label: 'Horizontal' },
|
||||||
|
{ value: 'vertical', label: 'Vertical' },
|
||||||
|
{ value: 'both', label: 'Both' }
|
||||||
|
];
|
||||||
|
|
||||||
const propData = [
|
const propData = [
|
||||||
{
|
{
|
||||||
name: 'text',
|
name: 'slot',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
default: '""',
|
default: '',
|
||||||
description: 'The text content to display inside the fuzzy text component.'
|
description: 'The text content to display inside the fuzzy text component.'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'fontSize',
|
name: 'fontSize',
|
||||||
type: 'number | string',
|
type: 'number | string',
|
||||||
default: '"clamp(2rem, 8vw, 8rem)"',
|
default: `"clamp(2rem, 8vw, 8rem)"`,
|
||||||
description:
|
description:
|
||||||
'Specifies the font size of the text. Accepts any valid CSS font-size value or a number (interpreted as pixels).'
|
'Specifies the font size of the text. Accepts any valid CSS font-size value or a number (interpreted as pixels).'
|
||||||
},
|
},
|
||||||
@@ -89,8 +138,8 @@ const propData = [
|
|||||||
{
|
{
|
||||||
name: 'fontFamily',
|
name: 'fontFamily',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
default: '"inherit"',
|
default: `"inherit"`,
|
||||||
description: 'Specifies the font family of the text. "inherit" uses the computed style from the parent.'
|
description: "Specifies the font family of the text. 'inherit' uses the computed style from the parent."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'color',
|
name: 'color',
|
||||||
@@ -115,6 +164,72 @@ const propData = [
|
|||||||
type: 'number',
|
type: 'number',
|
||||||
default: '0.5',
|
default: '0.5',
|
||||||
description: 'The fuzz intensity when the text is hovered.'
|
description: 'The fuzz intensity when the text is hovered.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'fuzzRange',
|
||||||
|
type: 'number',
|
||||||
|
default: '30',
|
||||||
|
description: 'Maximum pixel displacement for the fuzzy effect.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'fps',
|
||||||
|
type: 'number',
|
||||||
|
default: '60',
|
||||||
|
description: 'Frame rate cap for the animation. Lower values reduce CPU usage.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'direction',
|
||||||
|
type: `'horizontal' | 'vertical' | 'both'`,
|
||||||
|
default: `'horizontal'`,
|
||||||
|
description: 'The axis/axes for the fuzzy displacement effect.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'transitionDuration',
|
||||||
|
type: 'number',
|
||||||
|
default: '0',
|
||||||
|
description: 'Number of frames to ease between intensity states for smooth transitions.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'clickEffect',
|
||||||
|
type: 'boolean',
|
||||||
|
default: 'false',
|
||||||
|
description: 'Enables a momentary burst of maximum intensity on click.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'glitchMode',
|
||||||
|
type: 'boolean',
|
||||||
|
default: 'false',
|
||||||
|
description: 'Enables periodic random intensity spikes for a glitch effect.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'glitchInterval',
|
||||||
|
type: 'number',
|
||||||
|
default: '2000',
|
||||||
|
description: 'Milliseconds between glitch bursts when glitchMode is enabled.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'glitchDuration',
|
||||||
|
type: 'number',
|
||||||
|
default: '200',
|
||||||
|
description: 'Milliseconds duration of each glitch burst.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'gradient',
|
||||||
|
type: 'string[] | null',
|
||||||
|
default: 'null',
|
||||||
|
description: 'Array of colors to create a gradient text effect (e.g. ["#ff0000", "#00ff00"]).'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'letterSpacing',
|
||||||
|
type: 'number',
|
||||||
|
default: '0',
|
||||||
|
description: 'Extra pixels between characters.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'className',
|
||||||
|
type: 'string',
|
||||||
|
default: `''`,
|
||||||
|
description: 'CSS class for the canvas element.'
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -2,69 +2,77 @@
|
|||||||
<div class="gradient-text-demo">
|
<div class="gradient-text-demo">
|
||||||
<TabbedLayout>
|
<TabbedLayout>
|
||||||
<template #preview>
|
<template #preview>
|
||||||
<h2 class="demo-title-extra">Default</h2>
|
<div class="relative h-[400px] text-5xl demo-container">
|
||||||
|
|
||||||
<div class="demo-container h-[200px]">
|
|
||||||
<div class="text-[2rem]">
|
|
||||||
<GradientText
|
<GradientText
|
||||||
text="Add a splash of color!"
|
:colors="colors"
|
||||||
:colors="gradientPreview"
|
:animation-speed="animationSpeed"
|
||||||
:animation-speed="speed"
|
:direction="direction"
|
||||||
:show-border="false"
|
:pause-on-hover="pauseOnHover"
|
||||||
/>
|
:yoyo="yoyo"
|
||||||
</div>
|
:show-border="showBorder"
|
||||||
</div>
|
>
|
||||||
|
Gradient Magic
|
||||||
<h2 class="demo-title-extra">Border Animation</h2>
|
</GradientText>
|
||||||
|
|
||||||
<div class="demo-container h-[200px]">
|
|
||||||
<div class="text-[2rem]">
|
|
||||||
<GradientText
|
|
||||||
text="Now with a cool border!"
|
|
||||||
:colors="gradientPreview"
|
|
||||||
:animation-speed="speed"
|
|
||||||
:show-border="true"
|
|
||||||
class-name="custom-gradient-class"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Customize>
|
<Customize>
|
||||||
<PreviewSlider title="Loop Duration" v-model="speed" :min="1" :max="10" :step="0.5" value-unit="s" />
|
<PreviewSlider
|
||||||
|
title="Animation Speed"
|
||||||
|
v-model="animationSpeed"
|
||||||
|
:min="1"
|
||||||
|
:max="20"
|
||||||
|
:step="0.5"
|
||||||
|
value-unit="s"
|
||||||
|
/>
|
||||||
|
<PreviewSelect title="Direction" v-model="direction" :options="directionOptions" />
|
||||||
|
<PreviewSwitch title="Yoyo Mode" v-model="yoyo" />
|
||||||
|
<PreviewSwitch title="Pause on Hover" v-model="pauseOnHover" />
|
||||||
|
<PreviewSwitch title="Show Border" v-model="showBorder" />
|
||||||
|
|
||||||
<div class="flex flex-col gap-0">
|
<div class="flex flex-col gap-0">
|
||||||
<div class="mb-4">
|
<label class="block mb-2 font-medium text-sm">Colors</label>
|
||||||
<label class="block text-sm font-medium mb-2">Colors</label>
|
|
||||||
|
|
||||||
|
<div class="flex flex-wrap gap-2 px-1 pt-1">
|
||||||
|
<div v-for="(color, index) in colors" :key="index" class="relative w-8 h-8">
|
||||||
|
<div
|
||||||
|
class="relative border-[#222] border-2 rounded-md w-8 h-8 overflow-hidden"
|
||||||
|
:style="{ backgroundColor: color }"
|
||||||
|
>
|
||||||
<input
|
<input
|
||||||
v-model="colors"
|
type="color"
|
||||||
type="text"
|
:value="color"
|
||||||
placeholder="Enter colors separated by commas"
|
@input="updateColor(index, ($event.target as HTMLInputElement).value)"
|
||||||
maxlength="100"
|
class="absolute inset-0 opacity-0 w-8 h-8 cursor-pointer"
|
||||||
class="w-[300px] px-3 py-2 bg-[#0b0b0b] border border-[#333] rounded-md text-white focus:outline-none focus:border-[#666]"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
v-if="colors.length > 2"
|
||||||
|
@click="removeColor(index)"
|
||||||
|
class="-top-1.5 -right-1.5 absolute flex justify-center items-center bg-[#170D27] border border-[#222] rounded-full w-4 h-4 cursor-pointer"
|
||||||
|
>
|
||||||
|
<i class="text-[#8BC79A] text-[8px]! pi pi-times" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
v-if="colors.length < 8"
|
||||||
|
@click="addColor"
|
||||||
|
class="flex justify-center items-center border-[#392e4e] border-2 hover:border-[#27FF64] border-dashed rounded-md w-8 h-8 transition-colors cursor-pointer"
|
||||||
|
>
|
||||||
|
<i class="text-[#8BC79A] text-sm pi pi-plus" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="w-[300px] h-3 rounded-md border border-[#333333]"
|
class="mt-3 border border-[#333333] rounded-md w-[300px] h-3"
|
||||||
:style="{
|
:style="{
|
||||||
background: `linear-gradient(to right, ${gradientPreview.join(', ')})`
|
background: `linear-gradient(to right, ${gradientPreview})`
|
||||||
}"
|
}"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Customize>
|
</Customize>
|
||||||
|
|
||||||
<p class="demo-extra-info mt-4 flex items-center gap-2">
|
|
||||||
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
|
|
||||||
<path
|
|
||||||
fill-rule="evenodd"
|
|
||||||
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
For a smoother animation, the gradient should start and end with the same color.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<PropTable :data="propData" />
|
<PropTable :data="propData" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -80,27 +88,57 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed } from 'vue';
|
import CliInstallation from '@/components/code/CliInstallation.vue';
|
||||||
import TabbedLayout from '../../components/common/TabbedLayout.vue';
|
import CodeExample from '@/components/code/CodeExample.vue';
|
||||||
import PropTable from '../../components/common/PropTable.vue';
|
import Customize from '@/components/common/Customize.vue';
|
||||||
import CliInstallation from '../../components/code/CliInstallation.vue';
|
import PreviewSelect from '@/components/common/PreviewSelect.vue';
|
||||||
import CodeExample from '../../components/code/CodeExample.vue';
|
import PreviewSlider from '@/components/common/PreviewSlider.vue';
|
||||||
import Customize from '../../components/common/Customize.vue';
|
import PreviewSwitch from '@/components/common/PreviewSwitch.vue';
|
||||||
import PreviewSlider from '../../components/common/PreviewSlider.vue';
|
import PropTable from '@/components/common/PropTable.vue';
|
||||||
import GradientText from '../../content/TextAnimations/GradientText/GradientText.vue';
|
import TabbedLayout from '@/components/common/TabbedLayout.vue';
|
||||||
import { gradientText } from '@/constants/code/TextAnimations/gradientTextCode';
|
import { gradientText } from '@/constants/code/TextAnimations/gradientTextCode';
|
||||||
|
import GradientText from '@/content/TextAnimations/GradientText/GradientText.vue';
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
const colors = ref('#40ffaa, #4079ff, #40ffaa, #4079ff, #40ffaa');
|
const colors = ref(['#27FF64', '#27FF64', '#A0FFBC']);
|
||||||
const speed = ref(3);
|
const animationSpeed = ref(8);
|
||||||
|
const direction = ref<'horizontal' | 'vertical' | 'diagonal'>('horizontal');
|
||||||
|
const pauseOnHover = ref(false);
|
||||||
|
const yoyo = ref(true);
|
||||||
|
const showBorder = ref(false);
|
||||||
|
|
||||||
const gradientPreview = computed(() => colors.value.split(',').map(color => color.trim()));
|
const directionOptions = [
|
||||||
|
{ value: 'horizontal', label: 'Horizontal' },
|
||||||
|
{ value: 'vertical', label: 'Vertical' },
|
||||||
|
{ value: 'diagonal', label: 'Diagonal' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const gradientPreview = computed(() => [...colors.value, colors.value[0]].join(', '));
|
||||||
|
|
||||||
|
const updateColor = (index: number, newColor: string) => {
|
||||||
|
const newColors = [...colors.value];
|
||||||
|
newColors[index] = newColor;
|
||||||
|
colors.value = newColors;
|
||||||
|
};
|
||||||
|
|
||||||
|
const addColor = () => {
|
||||||
|
if (colors.value.length < 8) {
|
||||||
|
colors.value.push('#ffffff');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeColor = (index: number) => {
|
||||||
|
if (colors.value.length > 2) {
|
||||||
|
colors.value.splice(index, 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const propData = [
|
const propData = [
|
||||||
{
|
{
|
||||||
name: 'text',
|
name: 'slot',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
default: '""',
|
default: '-',
|
||||||
description: 'The text content to be displayed with gradient effect.'
|
description: 'The content to be displayed inside the gradient text.'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'className',
|
name: 'className',
|
||||||
@@ -111,20 +149,38 @@ const propData = [
|
|||||||
{
|
{
|
||||||
name: 'colors',
|
name: 'colors',
|
||||||
type: 'string[]',
|
type: 'string[]',
|
||||||
default: '["#ffaa40", "#9c40ff", "#ffaa40"]',
|
default: `["#5227FF", "#FF9FFC", "#B19EEF"]`,
|
||||||
description: 'Defines the gradient colors for the text or border.'
|
description: 'Array of colors for the gradient effect.'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'animationSpeed',
|
name: 'animationSpeed',
|
||||||
type: 'number',
|
type: 'number',
|
||||||
default: '8',
|
default: '8',
|
||||||
description: 'The duration of the gradient animation in seconds.'
|
description: 'Duration of one animation cycle in seconds.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'direction',
|
||||||
|
type: `'horizontal' | 'vertical' | 'diagonal'`,
|
||||||
|
default: `'horizontal'`,
|
||||||
|
description: 'Direction of the gradient animation.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'pauseOnHover',
|
||||||
|
type: 'boolean',
|
||||||
|
default: 'false',
|
||||||
|
description: 'Pauses the animation when hovering over the text.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'yoyo',
|
||||||
|
type: 'boolean',
|
||||||
|
default: 'true',
|
||||||
|
description: 'Reverses animation direction at the end instead of looping.'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'showBorder',
|
name: 'showBorder',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
default: 'false',
|
default: 'false',
|
||||||
description: 'Determines whether a border with the gradient effect is displayed.'
|
description: 'Displays a gradient border around the text.'
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,33 +1,31 @@
|
|||||||
<template>
|
<template>
|
||||||
<TabbedLayout>
|
<TabbedLayout>
|
||||||
<template #preview>
|
<template #preview>
|
||||||
<h2 class="demo-title-extra">Basic</h2>
|
<div class="h-[400px] text-3xl demo-container">
|
||||||
|
|
||||||
<div class="demo-container h-[200px]">
|
|
||||||
<ShinyText text="Just some shiny text!" :disabled="false" :speed="3" class-name="shiny-text-demo" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2 class="demo-title-extra">Button Text</h2>
|
|
||||||
|
|
||||||
<div class="demo-container h-[200px]">
|
|
||||||
<div class="shiny-button">
|
|
||||||
<ShinyText text="Shiny Button" :disabled="false" :speed="3" class-name="shiny-text-demo" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2 class="demo-title-extra">Configurable Speed</h2>
|
|
||||||
|
|
||||||
<div class="demo-container h-[200px]">
|
|
||||||
<ShinyText
|
<ShinyText
|
||||||
:text="speed < 2.5 ? '🐎 This is fast!' : '🐌 This is slow!'"
|
text="✨ Shiny Text Effect"
|
||||||
:disabled="false"
|
:delay="delay"
|
||||||
:speed="speed"
|
:speed="speed"
|
||||||
class-name="shiny-text-demo"
|
:color="color"
|
||||||
|
:shine-color="shineColor"
|
||||||
|
:spread="spread"
|
||||||
|
:direction="direction"
|
||||||
|
:yoyo="yoyo"
|
||||||
|
:pause-on-hover="pauseOnHover"
|
||||||
|
:disabled="disabled"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Customize>
|
<Customize>
|
||||||
<PreviewSlider title="Animation Duration" v-model="speed" :min="1" :max="5" :step="0.1" value-unit="s" />
|
<PreviewSlider title="Speed" v-model="speed" :min="0.5" :max="5" :step="0.1" value-unit="s" />
|
||||||
|
<PreviewSlider title="Delay" v-model="delay" :min="0" :max="3" :step="0.1" value-unit="s" />
|
||||||
|
<PreviewSlider title="Spread" v-model="spread" :min="0" :max="180" :step="5" value-unit="deg" />
|
||||||
|
<PreviewColor title="Color" v-model="color" class="mb-2" />
|
||||||
|
<PreviewColor title="Shine Color" v-model="shineColor" class="mb-2" />
|
||||||
|
<PreviewSelect title="Direction" v-model="direction" :options="directionOptions" />
|
||||||
|
<PreviewSwitch title="Yoyo Mode" v-model="yoyo" />
|
||||||
|
<PreviewSwitch title="Pause on Hover" v-model="pauseOnHover" />
|
||||||
|
<PreviewSwitch title="Disabled" v-model="disabled" />
|
||||||
</Customize>
|
</Customize>
|
||||||
|
|
||||||
<PropTable :data="propData" />
|
<PropTable :data="propData" />
|
||||||
@@ -44,17 +42,33 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue';
|
import CliInstallation from '@/components/code/CliInstallation.vue';
|
||||||
import TabbedLayout from '../../components/common/TabbedLayout.vue';
|
import CodeExample from '@/components/code/CodeExample.vue';
|
||||||
import PropTable from '../../components/common/PropTable.vue';
|
import Customize from '@/components/common/Customize.vue';
|
||||||
import CliInstallation from '../../components/code/CliInstallation.vue';
|
import PreviewColor from '@/components/common/PreviewColor.vue';
|
||||||
import CodeExample from '../../components/code/CodeExample.vue';
|
import PreviewSelect from '@/components/common/PreviewSelect.vue';
|
||||||
import Customize from '../../components/common/Customize.vue';
|
import PreviewSlider from '@/components/common/PreviewSlider.vue';
|
||||||
import PreviewSlider from '../../components/common/PreviewSlider.vue';
|
import PreviewSwitch from '@/components/common/PreviewSwitch.vue';
|
||||||
import ShinyText from '../../content/TextAnimations/ShinyText/ShinyText.vue';
|
import PropTable from '@/components/common/PropTable.vue';
|
||||||
|
import TabbedLayout from '@/components/common/TabbedLayout.vue';
|
||||||
import { shinyText } from '@/constants/code/TextAnimations/shinyTextCode';
|
import { shinyText } from '@/constants/code/TextAnimations/shinyTextCode';
|
||||||
|
import ShinyText from '@/content/TextAnimations/ShinyText/ShinyText.vue';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
const speed = ref(3);
|
const speed = ref(2);
|
||||||
|
const delay = ref(0);
|
||||||
|
const color = ref('#b5b5b5');
|
||||||
|
const shineColor = ref('#ffffff');
|
||||||
|
const spread = ref(120);
|
||||||
|
const direction = ref<'left' | 'right'>('left');
|
||||||
|
const yoyo = ref(false);
|
||||||
|
const pauseOnHover = ref(false);
|
||||||
|
const disabled = ref(false);
|
||||||
|
|
||||||
|
const directionOptions = [
|
||||||
|
{ value: 'left', label: 'Left' },
|
||||||
|
{ value: 'right', label: 'Right' }
|
||||||
|
];
|
||||||
|
|
||||||
const propData = [
|
const propData = [
|
||||||
{
|
{
|
||||||
@@ -64,16 +78,58 @@ const propData = [
|
|||||||
description: 'The text to be displayed with the shiny effect.'
|
description: 'The text to be displayed with the shiny effect.'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'disabled',
|
name: 'color',
|
||||||
type: 'boolean',
|
type: 'string',
|
||||||
default: 'false',
|
default: '"#b5b5b5"',
|
||||||
description: 'Disables the shiny effect when set to true.'
|
description: 'The base color of the text.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'shineColor',
|
||||||
|
type: 'string',
|
||||||
|
default: '"#ffffff"',
|
||||||
|
description: 'The color of the shine/highlight effect.'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'speed',
|
name: 'speed',
|
||||||
type: 'number',
|
type: 'number',
|
||||||
default: '5',
|
default: '2',
|
||||||
description: 'Specifies the duration of the animation in seconds.'
|
description: 'Duration of one animation cycle in seconds.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'delay',
|
||||||
|
type: 'number',
|
||||||
|
default: '0',
|
||||||
|
description: 'Pause duration (in seconds) between animation cycles.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'spread',
|
||||||
|
type: 'number',
|
||||||
|
default: '120',
|
||||||
|
description: 'The angle (in degrees) of the gradient spread.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'yoyo',
|
||||||
|
type: 'boolean',
|
||||||
|
default: 'false',
|
||||||
|
description: 'If true, the animation reverses direction instead of looping.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'pauseOnHover',
|
||||||
|
type: 'boolean',
|
||||||
|
default: 'false',
|
||||||
|
description: 'Pauses the animation when the user hovers over the text.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'direction',
|
||||||
|
type: "'left' | 'right'",
|
||||||
|
default: '"left"',
|
||||||
|
description: 'The direction the shine moves across the text.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'disabled',
|
||||||
|
type: 'boolean',
|
||||||
|
default: 'false',
|
||||||
|
description: 'Disables the shiny effect when set to true.'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'className',
|
name: 'className',
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<TabbedLayout>
|
<TabbedLayout>
|
||||||
<template #preview>
|
<template #preview>
|
||||||
<div class="demo-container h-[400px]">
|
<div class="h-[400px] demo-container">
|
||||||
<RefreshButton @refresh="forceRerender" />
|
<RefreshButton @refresh="forceRerender" />
|
||||||
|
|
||||||
<SplitText
|
<SplitText
|
||||||
@@ -11,8 +11,7 @@
|
|||||||
:duration="duration"
|
:duration="duration"
|
||||||
:ease="ease"
|
:ease="ease"
|
||||||
:split-type="splitType"
|
:split-type="splitType"
|
||||||
:threshold="threshold"
|
class-name="split-text-demo"
|
||||||
class="split-text-demo"
|
|
||||||
@animation-complete="
|
@animation-complete="
|
||||||
() => {
|
() => {
|
||||||
showCallback && showToast();
|
showCallback && showToast();
|
||||||
@@ -22,13 +21,27 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Customize>
|
<Customize>
|
||||||
<PreviewSwitch title="Show Completion Toast" v-model="showCallback" />
|
<div class="flex flex-wrap gap-4">
|
||||||
|
<button
|
||||||
|
class="bg-[#0b0b0b] hover:bg-[#222] px-3 border border-[#333] rounded-[10px] h-8 text-white text-xs transition-colors cursor-pointer"
|
||||||
|
@click="toggleSplitType"
|
||||||
|
>
|
||||||
|
Split Type:
|
||||||
|
<span class="text-[#a1a1aa]"> {{ splitType }}</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="bg-[#0b0b0b] hover:bg-[#222] px-3 border border-[#333] rounded-[10px] h-8 text-white text-xs transition-colors cursor-pointer"
|
||||||
|
@click="toggleEase"
|
||||||
|
>
|
||||||
|
Ease:
|
||||||
|
<span class="text-[#a1a1aa]"> {{ ease }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<PreviewSlider title="Stagger Delay (ms)" v-model="delay" :min="10" :max="500" :step="10" />
|
<PreviewSlider title="Stagger Delay (ms)" v-model="delay" :min="10" :max="500" :step="10" />
|
||||||
|
<PreviewSlider title="Duration (s)" v-model="duration" :min="0.1" :max="2" :step="0.1" />
|
||||||
<PreviewSlider title="Duration (s)" v-model="duration" :min="0.1" :max="3" :step="0.1" />
|
<PreviewSwitch title="Show Completion Toast" v-model="showCallback" />
|
||||||
|
|
||||||
<PreviewSlider title="Threshold" v-model="threshold" :min="0.1" :max="1" :step="0.1" />
|
|
||||||
</Customize>
|
</Customize>
|
||||||
|
|
||||||
<PropTable :data="propData" />
|
<PropTable :data="propData" />
|
||||||
@@ -47,29 +60,40 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, watch } from 'vue';
|
import CliInstallation from '@/components/code/CliInstallation.vue';
|
||||||
import TabbedLayout from '../../components/common/TabbedLayout.vue';
|
import CodeExample from '@/components/code/CodeExample.vue';
|
||||||
import RefreshButton from '../../components/common/RefreshButton.vue';
|
import Dependencies from '@/components/code/Dependencies.vue';
|
||||||
import PropTable from '../../components/common/PropTable.vue';
|
import Customize from '@/components/common/Customize.vue';
|
||||||
import Dependencies from '../../components/code/Dependencies.vue';
|
import PreviewSlider from '@/components/common/PreviewSlider.vue';
|
||||||
import CliInstallation from '../../components/code/CliInstallation.vue';
|
import PreviewSwitch from '@/components/common/PreviewSwitch.vue';
|
||||||
import CodeExample from '../../components/code/CodeExample.vue';
|
import PropTable from '@/components/common/PropTable.vue';
|
||||||
import Customize from '../../components/common/Customize.vue';
|
import RefreshButton from '@/components/common/RefreshButton.vue';
|
||||||
import PreviewSwitch from '../../components/common/PreviewSwitch.vue';
|
import TabbedLayout from '@/components/common/TabbedLayout.vue';
|
||||||
import PreviewSlider from '../../components/common/PreviewSlider.vue';
|
|
||||||
import SplitText from '../../content/TextAnimations/SplitText/SplitText.vue';
|
|
||||||
import { splitText } from '@/constants/code/TextAnimations/splitTextCode';
|
|
||||||
import { useToast } from 'primevue/usetoast';
|
|
||||||
import { useForceRerender } from '@/composables/useForceRerender';
|
import { useForceRerender } from '@/composables/useForceRerender';
|
||||||
|
import { splitText } from '@/constants/code/TextAnimations/splitTextCode';
|
||||||
|
import SplitText from '@/content/TextAnimations/SplitText/SplitText.vue';
|
||||||
|
import { useToast } from 'primevue/usetoast';
|
||||||
|
import { ref, watch } from 'vue';
|
||||||
|
|
||||||
const delay = ref(10);
|
|
||||||
const duration = ref(3);
|
|
||||||
const ease = ref('elastic.out(1, 0.3)');
|
|
||||||
const splitType = ref<'chars' | 'words' | 'lines' | 'words, chars'>('chars');
|
|
||||||
const threshold = ref(0.1);
|
|
||||||
const showCallback = ref(true);
|
|
||||||
const toast = useToast();
|
|
||||||
const { rerenderKey, forceRerender } = useForceRerender();
|
const { rerenderKey, forceRerender } = useForceRerender();
|
||||||
|
const toast = useToast();
|
||||||
|
|
||||||
|
const delay = ref(50);
|
||||||
|
const duration = ref(1.25);
|
||||||
|
const ease = ref<'power3.out' | 'bounce.out' | 'elastic.out(1, 0.3)'>('power3.out');
|
||||||
|
const splitType = ref<'chars' | 'words' | 'lines'>('chars');
|
||||||
|
const showCallback = ref(true);
|
||||||
|
|
||||||
|
const toggleSplitType = () => {
|
||||||
|
splitType.value = splitType.value === 'chars' ? 'words' : splitType.value === 'words' ? 'lines' : 'chars';
|
||||||
|
forceRerender();
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleEase = () => {
|
||||||
|
ease.value =
|
||||||
|
ease.value === 'power3.out' ? 'bounce.out' : ease.value === 'bounce.out' ? 'elastic.out(1, 0.3)' : 'power3.out';
|
||||||
|
forceRerender();
|
||||||
|
};
|
||||||
|
|
||||||
const showToast = () => {
|
const showToast = () => {
|
||||||
toast.add({
|
toast.add({
|
||||||
@@ -80,10 +104,31 @@ const showToast = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const propData = [
|
const propData = [
|
||||||
|
{
|
||||||
|
name: 'tag',
|
||||||
|
type: 'string',
|
||||||
|
default: '"p"',
|
||||||
|
description: 'HTML tag to render: "h1", "h2", "h3", "h4", "h5", "h6", "p",'
|
||||||
|
},
|
||||||
{ name: 'text', type: 'string', default: '""', description: 'The text content to animate.' },
|
{ name: 'text', type: 'string', default: '""', description: 'The text content to animate.' },
|
||||||
{ name: 'className', type: 'string', default: '""', description: 'Additional class names to style the component.' },
|
{
|
||||||
{ name: 'delay', type: 'number', default: '100', description: 'Delay between animations for each letter (in ms).' },
|
name: 'className',
|
||||||
{ name: 'duration', type: 'number', default: '0.6', description: 'Duration of each letter animation (in seconds).' },
|
type: 'string',
|
||||||
|
default: '""',
|
||||||
|
description: 'Additional class names to style the component.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'delay',
|
||||||
|
type: 'number',
|
||||||
|
default: '50',
|
||||||
|
description: 'Delay between animations for each letter (in ms).'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'duration',
|
||||||
|
type: 'number',
|
||||||
|
default: '1.25',
|
||||||
|
description: 'Duration of each letter animation (in seconds).'
|
||||||
|
},
|
||||||
{ name: 'ease', type: 'string', default: '"power3.out"', description: 'GSAP easing function for the animation.' },
|
{ name: 'ease', type: 'string', default: '"power3.out"', description: 'GSAP easing function for the animation.' },
|
||||||
{
|
{
|
||||||
name: 'splitType',
|
name: 'splitType',
|
||||||
@@ -114,7 +159,7 @@ const propData = [
|
|||||||
name: 'textAlign',
|
name: 'textAlign',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
default: '"center"',
|
default: '"center"',
|
||||||
description: 'Text alignment: "left", "center", "right", etc.'
|
description: "Text alignment: 'left', 'center', 'right', etc."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'onLetterAnimationComplete',
|
name: 'onLetterAnimationComplete',
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<TabbedLayout>
|
<TabbedLayout>
|
||||||
<template #preview>
|
<template #preview>
|
||||||
<div class="demo-container py-[64px] h-[350px]">
|
<div class="py-[64px] h-[350px] demo-container">
|
||||||
<TextType
|
<TextType
|
||||||
:key="key"
|
:key="key"
|
||||||
:text="texts"
|
:text="texts"
|
||||||
@@ -45,6 +45,7 @@
|
|||||||
:max="150"
|
:max="150"
|
||||||
:step="5"
|
:step="5"
|
||||||
value-unit="ms"
|
value-unit="ms"
|
||||||
|
:disabled="!variableSpeedEnabled"
|
||||||
/>
|
/>
|
||||||
<PreviewSlider
|
<PreviewSlider
|
||||||
v-model="variableSpeedMax"
|
v-model="variableSpeedMax"
|
||||||
@@ -53,6 +54,7 @@
|
|||||||
:max="300"
|
:max="300"
|
||||||
:step="5"
|
:step="5"
|
||||||
value-unit="ms"
|
value-unit="ms"
|
||||||
|
:disabled="!variableSpeedEnabled"
|
||||||
/>
|
/>
|
||||||
</Customize>
|
</Customize>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user