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

1 line
4.7 KiB
JSON

{"name":"PixelTransition","title":"PixelTransition","description":"Pixel dissolve transition for content reveal on hover.","type":"registry:component","add":"when-added","files":[{"type":"registry:component","role":"file","content":"<script setup lang=\"ts\">\nimport { ref, onMounted, watch, onUnmounted, nextTick, useTemplateRef } from 'vue';\nimport { gsap } from 'gsap';\n\ninterface PixelTransitionProps {\n gridSize?: number;\n pixelColor?: string;\n animationStepDuration?: number;\n className?: string;\n style?: Record<string, string | number>;\n aspectRatio?: string;\n}\n\nconst props = withDefaults(defineProps<PixelTransitionProps>(), {\n gridSize: 7,\n pixelColor: 'currentColor',\n animationStepDuration: 0.3,\n className: '',\n style: () => ({}),\n aspectRatio: '100%'\n});\n\nconst containerRef = useTemplateRef<HTMLDivElement>('containerRef');\nconst pixelGridRef = useTemplateRef<HTMLDivElement>('pixelGridRef');\nconst activeRef = useTemplateRef<HTMLDivElement>('activeRef');\nconst isActive = ref(false);\nlet delayedCall: gsap.core.Tween | null = null;\n\nconst isTouchDevice =\n typeof window !== 'undefined' &&\n ('ontouchstart' in window ||\n (navigator && navigator.maxTouchPoints > 0) ||\n (window.matchMedia && window.matchMedia('(pointer: coarse)').matches));\n\nfunction buildPixelGrid() {\n const pixelGridEl = pixelGridRef.value;\n if (!pixelGridEl) return;\n pixelGridEl.innerHTML = '';\n for (let row = 0; row < props.gridSize; row++) {\n for (let col = 0; col < props.gridSize; col++) {\n const pixel = document.createElement('div');\n pixel.classList.add('pixelated-image-card__pixel', 'absolute', 'hidden');\n pixel.style.backgroundColor = props.pixelColor;\n const size = 100 / props.gridSize;\n pixel.style.width = `${size}%`;\n pixel.style.height = `${size}%`;\n pixel.style.left = `${col * size}%`;\n pixel.style.top = `${row * size}%`;\n pixelGridEl.appendChild(pixel);\n }\n }\n}\n\nasync function animatePixels(activate: boolean) {\n isActive.value = activate;\n await nextTick();\n const pixelGridEl = pixelGridRef.value;\n const activeEl = activeRef.value;\n if (!pixelGridEl || !activeEl) return;\n const pixels = pixelGridEl.querySelectorAll<HTMLDivElement>('.pixelated-image-card__pixel');\n if (!pixels.length) return;\n gsap.killTweensOf(pixels);\n if (delayedCall) delayedCall.kill();\n gsap.set(pixels, { display: 'none' });\n const totalPixels = pixels.length;\n const staggerDuration = props.animationStepDuration / totalPixels;\n gsap.to(pixels, {\n display: 'block',\n duration: 0,\n stagger: {\n each: staggerDuration,\n from: 'random'\n }\n });\n delayedCall = gsap.delayedCall(props.animationStepDuration, () => {\n activeEl.style.display = activate ? 'block' : 'none';\n activeEl.style.pointerEvents = activate ? 'none' : '';\n });\n gsap.to(pixels, {\n display: 'none',\n duration: 0,\n delay: props.animationStepDuration,\n stagger: {\n each: staggerDuration,\n from: 'random'\n }\n });\n}\n\nfunction handleMouseEnter() {\n if (isTouchDevice) return;\n if (!isActive.value) animatePixels(true);\n}\nfunction handleMouseLeave() {\n if (isTouchDevice) return;\n if (isActive.value) animatePixels(false);\n}\nfunction handleClick() {\n if (!isTouchDevice) return;\n animatePixels(!isActive.value);\n}\n\nonMounted(async () => {\n await nextTick();\n buildPixelGrid();\n});\n\nwatch(\n () => [props.gridSize, props.pixelColor],\n () => {\n buildPixelGrid();\n }\n);\n\nonUnmounted(() => {\n if (delayedCall) delayedCall.kill();\n});\n</script>\n\n<template>\n <div\n ref=\"containerRef\"\n :class=\"[\n props.className,\n 'bg-[#222] text-white rounded-[15px] border-2 border-white w-[300px] max-w-full relative overflow-hidden'\n ]\"\n :style=\"props.style\"\n @mouseenter=\"handleMouseEnter\"\n @mouseleave=\"handleMouseLeave\"\n @click=\"handleClick\"\n >\n <div :style=\"{ paddingTop: props.aspectRatio }\" />\n\n <div class=\"absolute inset-0 w-full h-full\">\n <slot name=\"firstContent\" />\n </div>\n\n <div ref=\"activeRef\" class=\"absolute inset-0 w-full h-full z-[2]\" style=\"display: none\">\n <slot name=\"secondContent\" />\n </div>\n\n <div ref=\"pixelGridRef\" class=\"absolute inset-0 w-full h-full pointer-events-none z-[3]\" />\n </div>\n</template>\n\n<style scoped>\n.pixelated-image-card__pixel {\n transition: none;\n}\n</style>\n","path":"PixelTransition/PixelTransition.vue","_imports_":[],"registryDependencies":[],"dependencies":[],"devDependencies":[]}],"registryDependencies":[],"dependencies":[{"ecosystem":"js","name":"gsap","version":"^3.13.0"}],"devDependencies":[],"categories":["Animations"]}