reborn-ui 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/README.md +47 -0
  2. package/dist/index.js +871 -0
  3. package/dist/index.js.map +1 -0
  4. package/package.json +40 -0
  5. package/registry/.gitkeep +2 -0
  6. package/registry/components/animate-grid.json +18 -0
  7. package/registry/components/animated-beam.json +16 -0
  8. package/registry/components/animated-circular-progressbar.json +16 -0
  9. package/registry/components/animated-list.json +22 -0
  10. package/registry/components/animated-testimonials.json +18 -0
  11. package/registry/components/animated-tooltip.json +18 -0
  12. package/registry/components/apple-card-carousel.json +35 -0
  13. package/registry/components/aurora-background.json +16 -0
  14. package/registry/components/balance-slider.json +16 -0
  15. package/registry/components/bending-gallery.json +18 -0
  16. package/registry/components/bento-grid.json +24 -0
  17. package/registry/components/bg-black-hole.json +14 -0
  18. package/registry/components/bg-bubbles.json +18 -0
  19. package/registry/components/bg-falling-stars.json +16 -0
  20. package/registry/components/bg-neural.json +14 -0
  21. package/registry/components/bg-particle-whirlpool.json +18 -0
  22. package/registry/components/bg-silk.json +16 -0
  23. package/registry/components/bg-stars.json +18 -0
  24. package/registry/components/bg-stractium.json +16 -0
  25. package/registry/components/blur-reveal.json +18 -0
  26. package/registry/components/book.json +28 -0
  27. package/registry/components/border-beam.json +16 -0
  28. package/registry/components/box-reveal.json +18 -0
  29. package/registry/components/card-3d.json +24 -0
  30. package/registry/components/card-spotlight.json +16 -0
  31. package/registry/components/carousel-3d.json +15 -0
  32. package/registry/components/color-picker.json +26 -0
  33. package/registry/components/colourful-text.json +18 -0
  34. package/registry/components/compare.json +22 -0
  35. package/registry/components/confetti.json +22 -0
  36. package/registry/components/container-scroll.json +26 -0
  37. package/registry/components/container-text-flip.json +19 -0
  38. package/registry/components/cosmic-portal.json +18 -0
  39. package/registry/components/direction-aware-hover.json +16 -0
  40. package/registry/components/dock.json +32 -0
  41. package/registry/components/expandable-gallery.json +16 -0
  42. package/registry/components/file-tree.json +28 -0
  43. package/registry/components/file-upload.json +22 -0
  44. package/registry/components/flickering-grid.json +16 -0
  45. package/registry/components/flip-card.json +16 -0
  46. package/registry/components/flip-words.json +16 -0
  47. package/registry/components/fluid-cursor.json +16 -0
  48. package/registry/components/focus.json +16 -0
  49. package/registry/components/github-globe.json +23 -0
  50. package/registry/components/glare-card.json +18 -0
  51. package/registry/components/globe.json +19 -0
  52. package/registry/components/glow-border.json +16 -0
  53. package/registry/components/glowing-effect.json +18 -0
  54. package/registry/components/gradient-button.json +16 -0
  55. package/registry/components/halo-search.json +16 -0
  56. package/registry/components/hyper-text.json +19 -0
  57. package/registry/components/icon-cloud.json +16 -0
  58. package/registry/components/image-trail-cursor.json +22 -0
  59. package/registry/components/images-slider.json +18 -0
  60. package/registry/components/infinite-grid.json +47 -0
  61. package/registry/components/input.json +18 -0
  62. package/registry/components/interactive-grid-pattern.json +16 -0
  63. package/registry/components/interactive-hover-button.json +16 -0
  64. package/registry/components/iphone-mockup.json +16 -0
  65. package/registry/components/lamp-effect.json +16 -0
  66. package/registry/components/lens.json +18 -0
  67. package/registry/components/letter-pullup.json +18 -0
  68. package/registry/components/light-speed.json +31 -0
  69. package/registry/components/line-shadow-text.json +16 -0
  70. package/registry/components/link-preview.json +16 -0
  71. package/registry/components/liquid-background.json +18 -0
  72. package/registry/components/liquid-glass.json +16 -0
  73. package/registry/components/liquid-logo.json +24 -0
  74. package/registry/components/logo-cloud.json +24 -0
  75. package/registry/components/logo-origami.json +22 -0
  76. package/registry/components/marquee.json +20 -0
  77. package/registry/components/meteors.json +16 -0
  78. package/registry/components/morphing-tabs.json +16 -0
  79. package/registry/components/morphing-text.json +16 -0
  80. package/registry/components/multi-step-loader.json +16 -0
  81. package/registry/components/neon-border.json +16 -0
  82. package/registry/components/number-ticker.json +18 -0
  83. package/registry/components/orbit.json +16 -0
  84. package/registry/components/particle-image.json +24 -0
  85. package/registry/components/particles-bg.json +18 -0
  86. package/registry/components/pattern-background.json +18 -0
  87. package/registry/components/photo-gallery.json +16 -0
  88. package/registry/components/radiant-text.json +16 -0
  89. package/registry/components/rainbow-button.json +16 -0
  90. package/registry/components/ripple-button.json +16 -0
  91. package/registry/components/ripple.json +24 -0
  92. package/registry/components/safari-mockup.json +16 -0
  93. package/registry/components/scratch-to-reveal.json +18 -0
  94. package/registry/components/scroll-island.json +20 -0
  95. package/registry/components/shader-toy.json +22 -0
  96. package/registry/components/shimmer-button.json +16 -0
  97. package/registry/components/sleek-line-cursor.json +12 -0
  98. package/registry/components/smooth-cursor.json +23 -0
  99. package/registry/components/snowfall-bg.json +18 -0
  100. package/registry/components/sparkles-text.json +18 -0
  101. package/registry/components/sparkles.json +18 -0
  102. package/registry/components/spinning-text.json +18 -0
  103. package/registry/components/spline.json +23 -0
  104. package/registry/components/spring-calendar.json +22 -0
  105. package/registry/components/svg-mask.json +16 -0
  106. package/registry/components/tailed-cursor.json +14 -0
  107. package/registry/components/testimonial-slider.json +16 -0
  108. package/registry/components/tetris.json +19 -0
  109. package/registry/components/text-3d.json +16 -0
  110. package/registry/components/text-generate-effect.json +16 -0
  111. package/registry/components/text-glitch.json +12 -0
  112. package/registry/components/text-highlight.json +16 -0
  113. package/registry/components/text-hover-effect.json +16 -0
  114. package/registry/components/text-reveal-card.json +20 -0
  115. package/registry/components/text-reveal.json +18 -0
  116. package/registry/components/text-scroll-reveal.json +20 -0
  117. package/registry/components/timeline.json +18 -0
  118. package/registry/components/tracing-beam.json +19 -0
  119. package/registry/components/vanishing-input.json +18 -0
  120. package/registry/components/video-text.json +16 -0
  121. package/registry/components/vortex.json +19 -0
  122. package/registry/components/warp-background.json +22 -0
  123. package/registry/components/wavy-background.json +19 -0
  124. package/registry/components/world-map.json +19 -0
  125. package/registry/registry.json +2007 -0
  126. package/templates/composables/useMouseState.ts +21 -0
  127. package/templates/lib/utils.ts +13 -0
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "glare-card",
3
+ "dependencies": [
4
+ "@vueuse/core"
5
+ ],
6
+ "files": [
7
+ {
8
+ "path": "GlareCard.vue",
9
+ "content": "<template>\r\n <div\r\n ref=\"refElement\"\r\n class=\"container-style duration-[var(--duration)] ease-[var(--easing)] delay-[var(--delay)] container relative isolate w-[320px] transition-transform will-change-transform [aspect-ratio:17/21] [contain:layout_style] [perspective:600px]\"\r\n @pointermove=\"handlePointerMove\"\r\n @pointerenter=\"handlePointerEnter\"\r\n @pointerleave=\"handlePointerLeave\"\r\n >\r\n <div\r\n class=\"duration-[var(--duration)] ease-[var(--easing)] delay-[var(--delay)] grid h-full origin-center overflow-hidden rounded-lg border border-slate-800 transition-transform will-change-transform [transform:rotateY(var(--r-x))_rotateX(var(--r-y))] hover:filter-none hover:[--duration:200ms] hover:[--easing:linear] hover:[--opacity:0.6]\"\r\n >\r\n <div\r\n class=\"grid size-full mix-blend-soft-light [clip-path:inset(0_0_0_0_round_var(--radius))] [grid-area:1/1]\"\r\n >\r\n <div :class=\"cn('size-full bg-slate-950', props.class)\">\r\n <slot />\r\n </div>\r\n </div>\r\n <div\r\n class=\"transition-background duration-[var(--duration)] ease-[var(--easing)] delay-[var(--delay)] will-change-background grid size-full opacity-[var(--opacity)] mix-blend-soft-light transition-opacity [background:radial-gradient(farthest-corner_circle_at_var(--m-x)_var(--m-y),_rgba(255,255,255,0.8)_10%,_rgba(255,255,255,0.65)_20%,_rgba(255,255,255,0)_90%)] [clip-path:inset(0_0_1px_0_round_var(--radius))] [grid-area:1/1]\"\r\n />\r\n <div\r\n class=\"background-style will-change-background after:grid-area-[inherit] after:bg-repeat-[inherit] after:bg-attachment-[inherit] after:bg-origin-[inherit] after:bg-clip-[inherit] relative grid size-full opacity-[var(--opacity)] mix-blend-color-dodge transition-opacity [background-blend-mode:hue_hue_hue_overlay] [background:var(--pattern),_var(--rainbow),_var(--diagonal),_var(--shade)] [clip-path:inset(0_0_1px_0_round_var(--radius))] [grid-area:1/1] after:bg-[inherit] after:mix-blend-exclusion after:content-[\\'\\'] after:[background-blend-mode:soft-light,_hue,_hard-light] after:[background-position:center,_0%_var(--bg-y),_calc(var(--bg-x)*_-1)_calc(var(--bg-y)*_-1),_var(--bg-x)_var(--bg-y)] after:[background-size:var(--foil-size),_200%_400%,_800%,_200%]\"\r\n />\r\n </div>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { cn } from \"@/lib/utils\";\r\nimport { useTimeoutFn } from \"@vueuse/core\";\r\nimport { ref } from \"vue\";\r\n\r\ninterface GlareCardProps {\r\n class?: string;\r\n}\r\n\r\nconst props = defineProps<GlareCardProps>();\r\n\r\nconst isPointerInside = ref(false);\r\nconst refElement = ref<HTMLElement | null>(null);\r\n\r\nconst state = ref({\r\n glare: { x: 50, y: 50 },\r\n background: { x: 50, y: 50 },\r\n rotate: { x: 0, y: 0 },\r\n});\r\n\r\nfunction handlePointerMove(event: PointerEvent) {\r\n const rotateFactor = 0.4;\r\n const rect = refElement.value?.getBoundingClientRect();\r\n if (rect) {\r\n const position = {\r\n x: event.clientX - rect.left,\r\n y: event.clientY - rect.top,\r\n };\r\n const percentage = {\r\n x: (100 / rect.width) * position.x,\r\n y: (100 / rect.height) * position.y,\r\n };\r\n const delta = {\r\n x: percentage.x - 50,\r\n y: percentage.y - 50,\r\n };\r\n state.value.background.x = 50 + percentage.x / 4 - 12.5;\r\n state.value.background.y = 50 + percentage.y / 3 - 16.67;\r\n state.value.rotate.x = -(delta.x / 3.5) * rotateFactor;\r\n state.value.rotate.y = (delta.y / 2) * rotateFactor;\r\n state.value.glare.x = percentage.x;\r\n state.value.glare.y = percentage.y;\r\n }\r\n}\r\n\r\nfunction handlePointerEnter() {\r\n isPointerInside.value = true;\r\n useTimeoutFn(() => {\r\n if (isPointerInside.value && refElement.value) {\r\n refElement.value.style.setProperty(\"--duration\", \"0s\");\r\n }\r\n }, 300);\r\n}\r\n\r\nfunction handlePointerLeave() {\r\n isPointerInside.value = false;\r\n if (refElement.value) {\r\n refElement.value.style.removeProperty(\"--duration\");\r\n state.value.rotate = { x: 0, y: 0 };\r\n }\r\n}\r\n</script>\r\n\r\n<style scoped>\r\n.background-style {\r\n --step: 5%;\r\n --foil-svg: url(\"data:image/svg+xml,%3Csvg width='26' height='26' viewBox='0 0 26 26' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M2.99994 3.419C2.99994 3.419 21.6142 7.43646 22.7921 12.153C23.97 16.8695 3.41838 23.0306 3.41838 23.0306' stroke='white' stroke-width='5' stroke-miterlimit='3.86874' stroke-linecap='round' style='mix-blend-mode:darken'/%3E%3C/svg%3E\");\r\n --pattern: var(--foil-svg) center/100% no-repeat;\r\n --rainbow: repeating-linear-gradient(\r\n 0deg,\r\n rgb(255, 119, 115) calc(var(--step) * 1),\r\n rgba(255, 237, 95, 1) calc(var(--step) * 2),\r\n rgba(168, 255, 95, 1) calc(var(--step) * 3),\r\n rgba(131, 255, 247, 1) calc(var(--step) * 4),\r\n rgba(120, 148, 255, 1) calc(var(--step) * 5),\r\n rgb(216, 117, 255) calc(var(--step) * 6),\r\n rgb(255, 119, 115) calc(var(--step) * 7)\r\n )\r\n 0% var(--bg-y) / 200% 700% no-repeat;\r\n --diagonal: repeating-linear-gradient(\r\n 128deg,\r\n #0e152e 0%,\r\n hsl(180, 10%, 60%) 3.8%,\r\n hsl(180, 10%, 60%) 4.5%,\r\n hsl(180, 10%, 60%) 5.2%,\r\n #0e152e 10%,\r\n #0e152e 12%\r\n )\r\n var(--bg-x) var(--bg-y) / 300% no-repeat;\r\n --shade: radial-gradient(\r\n farthest-corner circle at var(--m-x) var(--m-y),\r\n rgba(255, 255, 255, 0.1) 12%,\r\n rgba(255, 255, 255, 0.15) 20%,\r\n rgba(255, 255, 255, 0.25) 120%\r\n )\r\n var(--bg-x) var(--bg-y) / 300% no-repeat;\r\n background-blend-mode: hue, hue, hue, overlay;\r\n}\r\n.container-style {\r\n --m-x: v-bind(state.glare.x + \"%\");\r\n --m-y: v-bind(state.glare.y + \"%\");\r\n --r-x: v-bind(state.rotate.x + \"deg\");\r\n --r-y: v-bind(state.rotate.y + \"deg\");\r\n --bg-x: v-bind(state.background.x + \"%\");\r\n --bg-y: v-bind(state.background.y + \"%\");\r\n --duration: 300ms;\r\n --foil-size: 100%;\r\n --opacity: 0;\r\n --radius: 48px;\r\n --easing: ease;\r\n --transition: var(--duration) var(--easing);\r\n}\r\n</style>\r\n"
10
+ },
11
+ {
12
+ "path": "index.ts",
13
+ "content": "export { default as GlareCard } from \"./GlareCard.vue\";\r\n"
14
+ }
15
+ ],
16
+ "fileCount": 2,
17
+ "contentHash": "ce4b469114d1bc37c437ad517c9329e0b79e9036"
18
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "globe",
3
+ "dependencies": [
4
+ "cobe",
5
+ "vue-use-spring"
6
+ ],
7
+ "files": [
8
+ {
9
+ "path": "Globe.vue",
10
+ "content": "<template>\r\n <div :class=\"cn('absolute inset-0 mx-auto aspect-[1/1] w-full max-w-[600px]', $props.class)\">\r\n <canvas\r\n ref=\"globeCanvasRef\"\r\n class=\"size-full opacity-0 transition-opacity duration-1000 ease-in-out [contain:layout_paint_size]\"\r\n @pointerdown=\"(e) => updatePointerInteraction(e.clientX)\"\r\n @pointerup=\"updatePointerInteraction(null)\"\r\n @pointerout=\"updatePointerInteraction(null)\"\r\n @mousemove=\"(e) => updateMovement(e.clientX)\"\r\n @touchmove=\"(e) => e.touches[0] && updateMovement(e.touches[0].clientX)\"\r\n ></canvas>\r\n </div>\r\n</template>\r\n\r\n<script lang=\"ts\" setup>\r\nimport { cn } from \"@/lib/utils\";\r\nimport createGlobe, { type COBEOptions } from \"cobe\";\r\nimport { useSpring } from \"vue-use-spring\";\r\nimport { ref, onMounted, onBeforeUnmount } from \"vue\";\r\n\r\ntype GlobeProps = {\r\n class?: string;\r\n config?: Partial<COBEOptions>;\r\n mass?: number;\r\n tension?: number;\r\n friction?: number;\r\n precision?: number;\r\n};\r\n\r\nconst DEFAULT_CONFIG: COBEOptions = {\r\n width: 800,\r\n height: 800,\r\n onRender: () => {},\r\n devicePixelRatio: 2,\r\n phi: 0,\r\n theta: 0.3,\r\n dark: 0,\r\n diffuse: 0.4,\r\n mapSamples: 16000,\r\n mapBrightness: 1.2,\r\n baseColor: [1, 1, 1],\r\n markerColor: [251 / 255, 100 / 255, 21 / 255],\r\n glowColor: [1.2, 1.2, 1.2],\r\n markers: [\r\n { location: [14.5995, 120.9842], size: 0.03 },\r\n { location: [19.076, 72.8777], size: 0.1 },\r\n { location: [23.8103, 90.4125], size: 0.05 },\r\n { location: [30.0444, 31.2357], size: 0.07 },\r\n { location: [39.9042, 116.4074], size: 0.08 },\r\n { location: [-23.5505, -46.6333], size: 0.1 },\r\n { location: [19.4326, -99.1332], size: 0.1 },\r\n { location: [40.7128, -74.006], size: 0.1 },\r\n { location: [34.6937, 135.5022], size: 0.05 },\r\n { location: [41.0082, 28.9784], size: 0.06 },\r\n ],\r\n};\r\n\r\nconst props = withDefaults(defineProps<GlobeProps>(), {\r\n mass: 1,\r\n tension: 280,\r\n friction: 100,\r\n precision: 0.001,\r\n});\r\n\r\nconst globeCanvasRef = ref<HTMLCanvasElement>();\r\nconst phi = ref(0);\r\nconst width = ref(0);\r\nconst pointerInteracting = ref();\r\nconst pointerInteractionMovement = ref();\r\n\r\nlet globe: ReturnType<typeof createGlobe> | null = null;\r\n\r\nconst spring = useSpring(\r\n {\r\n r: 0,\r\n },\r\n {\r\n mass: props.mass,\r\n tension: props.tension,\r\n friction: props.friction,\r\n precision: props.precision,\r\n },\r\n);\r\n\r\nfunction updatePointerInteraction(clientX: number | null) {\r\n if (clientX !== null) {\r\n pointerInteracting.value = clientX - (pointerInteractionMovement.value ?? clientX);\r\n } else {\r\n pointerInteracting.value = null;\r\n }\r\n\r\n if (globeCanvasRef.value) {\r\n globeCanvasRef.value.style.cursor = clientX ? \"grabbing\" : \"grab\";\r\n }\r\n}\r\n\r\nfunction updateMovement(clientX: number) {\r\n if (pointerInteracting.value !== null) {\r\n const delta = clientX - (pointerInteracting.value ?? clientX);\r\n pointerInteractionMovement.value = delta;\r\n spring.r = delta / 200;\r\n }\r\n}\r\n\r\nfunction onRender(state: Record<string, unknown>) {\r\n if (!pointerInteracting.value) {\r\n phi.value += 0.005;\r\n }\r\n\r\n state.phi = phi.value + spring.r;\r\n state.width = width.value * 2;\r\n state.height = width.value * 2;\r\n}\r\n\r\nfunction onResize() {\r\n if (globeCanvasRef.value) {\r\n width.value = globeCanvasRef.value.offsetWidth;\r\n }\r\n}\r\n\r\nfunction createGlobeOnMounted() {\r\n const config = { ...DEFAULT_CONFIG, ...props.config };\r\n\r\n globe = createGlobe(globeCanvasRef.value!, {\r\n ...config,\r\n width: width.value * 2,\r\n height: width.value * 2,\r\n onRender,\r\n });\r\n}\r\n\r\nonMounted(() => {\r\n window.addEventListener(\"resize\", onResize);\r\n onResize();\r\n createGlobeOnMounted();\r\n\r\n setTimeout(() => (globeCanvasRef.value!.style.opacity = \"1\"));\r\n});\r\n\r\nonBeforeUnmount(() => {\r\n globe?.destroy();\r\n window.removeEventListener(\"resize\", onResize);\r\n});\r\n</script>\r\n"
11
+ },
12
+ {
13
+ "path": "index.ts",
14
+ "content": "export { default as Globe } from \"./Globe.vue\";\r\n"
15
+ }
16
+ ],
17
+ "fileCount": 2,
18
+ "contentHash": "3b87bf2da7dd6e410f9d0b4852858f6bbaac98bf"
19
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "glow-border",
3
+ "dependencies": [],
4
+ "files": [
5
+ {
6
+ "path": "GlowBorder.vue",
7
+ "content": "<template>\r\n <div\r\n :style=\"styles\"\r\n :class=\"\r\n cn(\r\n 'pointer-events-none absolute inset-0 size-full rounded-[inherit] will-change-[background-position] motion-safe:animate-glow',\r\n props.class,\r\n )\r\n \"\r\n ></div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { cn } from \"@/lib/utils\";\r\nimport { computed, type HTMLAttributes } from \"vue\";\r\n\r\ninterface Props {\r\n borderRadius?: number;\r\n color?: string | Array<string>;\r\n borderWidth?: number;\r\n duration?: number;\r\n class?: HTMLAttributes[\"class\"];\r\n}\r\n\r\nconst props = withDefaults(defineProps<Props>(), {\r\n borderRadius: 10,\r\n color: \"#FFF\",\r\n borderWidth: 2,\r\n duration: 10,\r\n});\r\n\r\nconst styles = computed(() => {\r\n return {\r\n \"--border-radius\": `${props.borderRadius}px`,\r\n \"--border-width\": `${props.borderWidth}px`,\r\n \"--duration\": `${props.duration}s`,\r\n backgroundImage: `radial-gradient(transparent,transparent, ${\r\n Array.isArray(props.color) ? props.color.join(\",\") : props.color\r\n },transparent,transparent)`,\r\n backgroundSize: \"300% 300%\",\r\n mask: `linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0)`,\r\n WebkitMask: `linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0)`,\r\n WebkitMaskComposite: \"xor\",\r\n maskComposite: \"exclude\",\r\n padding: \"var(--border-width)\",\r\n borderRadius: \"var(--border-radius)\",\r\n };\r\n});\r\n</script>\r\n"
8
+ },
9
+ {
10
+ "path": "index.ts",
11
+ "content": "export { default as GlowBorder } from \"./GlowBorder.vue\";\r\n"
12
+ }
13
+ ],
14
+ "fileCount": 2,
15
+ "contentHash": "39d26df8462dc61b81be5a851c7f44b810ffb8f3"
16
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "glowing-effect",
3
+ "dependencies": [
4
+ "motion-v"
5
+ ],
6
+ "files": [
7
+ {
8
+ "path": "GlowingEffect.vue",
9
+ "content": "<template>\r\n <div\r\n :class=\"\r\n cn(\r\n 'pointer-events-none absolute -inset-px hidden rounded-[inherit] border opacity-0 transition-opacity',\r\n glow && 'opacity-100',\r\n variant === 'white' && 'border-white',\r\n disabled && '!block',\r\n )\r\n \"\r\n />\r\n <div\r\n ref=\"containerRef\"\r\n :style=\"containerStyles\"\r\n :class=\"\r\n cn(\r\n 'pointer-events-none absolute inset-0 rounded-[inherit] opacity-100 transition-opacity',\r\n glow && 'opacity-100',\r\n blur > 0 && 'blur-[var(--blur)]',\r\n props.class,\r\n disabled && '!hidden',\r\n )\r\n \"\r\n >\r\n <div\r\n :class=\"\r\n cn(\r\n 'glow',\r\n 'rounded-[inherit]',\r\n `after:content-[''] after:rounded-[inherit] after:absolute after:inset-[calc(-1*var(--glowingeffect-border-width))]`,\r\n 'after:[border:var(--glowingeffect-border-width)_solid_transparent]',\r\n 'after:[background:var(--gradient)] after:[background-attachment:fixed]',\r\n 'after:opacity-[var(--active)] after:transition-opacity after:duration-300',\r\n 'after:[mask-clip:padding-box,border-box]',\r\n 'after:[mask-composite:intersect]',\r\n 'after:[mask-image:linear-gradient(#0000,#0000),conic-gradient(from_calc((var(--start)-var(--spread))*1deg),#00000000_0deg,#fff,#00000000_calc(var(--spread)*2deg))]',\r\n )\r\n \"\r\n />\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { cn } from \"@/lib/utils\";\r\nimport { animate } from \"motion-v\";\r\nimport type { HTMLAttributes } from \"vue\";\r\n\r\ninterface Props {\r\n blur?: number;\r\n inactiveZone?: number;\r\n proximity?: number;\r\n spread?: number;\r\n variant?: \"default\" | \"white\";\r\n glow?: boolean;\r\n class?: HTMLAttributes[\"class\"];\r\n disabled?: boolean;\r\n movementDuration?: number;\r\n borderWidth?: number;\r\n}\r\n\r\nconst props = withDefaults(defineProps<Props>(), {\r\n blur: 0,\r\n inactiveZone: 0.7,\r\n proximity: 0,\r\n spread: 20,\r\n variant: \"default\",\r\n glow: false,\r\n movementDuration: 2,\r\n borderWidth: 1,\r\n disabled: true,\r\n});\r\n\r\nconst containerRef = templateRef(\"containerRef\");\r\nconst lastPosition = ref({\r\n x: 0,\r\n y: 0,\r\n});\r\nconst animationFrame = ref(0);\r\n\r\nconst containerStyles = computed(() => {\r\n return {\r\n \"--blur\": `${props.blur}px`,\r\n \"--spread\": props.spread,\r\n \"--start\": \"0\",\r\n \"--active\": \"0\",\r\n \"--glowingeffect-border-width\": `${props.borderWidth}px`,\r\n \"--repeating-conic-gradient-times\": \"5\",\r\n \"--gradient\":\r\n props.variant === \"white\"\r\n ? `repeating-conic-gradient(\r\n from 236.84deg at 50% 50%,\r\n var(--black),\r\n var(--black) calc(25% / var(--repeating-conic-gradient-times))\r\n )`\r\n : `radial-gradient(circle, #dd7bbb 10%, #dd7bbb00 20%),\r\n radial-gradient(circle at 40% 40%, #d79f1e 5%, #d79f1e00 15%),\r\n radial-gradient(circle at 60% 60%, #5a922c 10%, #5a922c00 20%), \r\n radial-gradient(circle at 40% 60%, #4c7894 10%, #4c789400 20%),\r\n repeating-conic-gradient(\r\n from 236.84deg at 50% 50%,\r\n #dd7bbb 0%,\r\n #d79f1e calc(25% / var(--repeating-conic-gradient-times)),\r\n #5a922c calc(50% / var(--repeating-conic-gradient-times)), \r\n #4c7894 calc(75% / var(--repeating-conic-gradient-times)),\r\n #dd7bbb calc(100% / var(--repeating-conic-gradient-times))\r\n )`,\r\n };\r\n});\r\n\r\nonMounted(() => {\r\n if (props.disabled) return;\r\n\r\n window.addEventListener(\"scroll\", handleScroll, { passive: true });\r\n document.body.addEventListener(\"pointermove\", handlePointerMove, {\r\n passive: true,\r\n });\r\n});\r\n\r\nonUnmounted(() => {\r\n if (animationFrame.value) {\r\n cancelAnimationFrame(animationFrame.value);\r\n }\r\n\r\n window.removeEventListener(\"scroll\", handleScroll);\r\n document.body.removeEventListener(\"pointermove\", handlePointerMove);\r\n});\r\n\r\nfunction handlePointerMove(e: PointerEvent) {\r\n handleMove(e);\r\n}\r\n\r\nfunction handleScroll() {\r\n handleMove();\r\n}\r\n\r\nfunction handleMove(e?: MouseEvent | PointerEvent | { x: number; y: number }) {\r\n if (!containerRef.value) return;\r\n\r\n if (animationFrame.value) {\r\n cancelAnimationFrame(animationFrame.value);\r\n }\r\n\r\n animationFrame.value = requestAnimationFrame(() => {\r\n const element = containerRef.value;\r\n\r\n if (!element) return;\r\n\r\n const { left, top, width, height } = element.getBoundingClientRect();\r\n\r\n const mouseX = e?.x ?? lastPosition.value.x;\r\n const mouseY = e?.y ?? lastPosition.value.y;\r\n\r\n if (e) {\r\n lastPosition.value = { x: mouseX, y: mouseY };\r\n }\r\n\r\n const center = [left + width * 0.5, top + height * 0.5];\r\n const distanceFromCenter = Math.hypot(mouseX - center[0], mouseY - center[1]);\r\n const inactiveRadius = 0.5 * Math.min(width, height) * props.inactiveZone;\r\n\r\n if (distanceFromCenter < inactiveRadius) {\r\n element.style.setProperty(\"--active\", \"0\");\r\n return;\r\n }\r\n\r\n const isActive =\r\n mouseX > left - props.proximity &&\r\n mouseX < left + width + props.proximity &&\r\n mouseY > top - props.proximity &&\r\n mouseY < top + height + props.proximity;\r\n\r\n element.style.setProperty(\"--active\", isActive ? \"1\" : \"0\");\r\n\r\n if (!isActive) return;\r\n\r\n const currentAngle = parseFloat(element.style.getPropertyValue(\"--start\")) || 0;\r\n let targetAngle = (180 * Math.atan2(mouseY - center[1], mouseX - center[0])) / Math.PI + 90;\r\n\r\n const angleDiff = ((targetAngle - currentAngle + 180) % 360) - 180;\r\n const newAngle = currentAngle + angleDiff;\r\n\r\n animate(currentAngle, newAngle, {\r\n duration: props.movementDuration,\r\n ease: [0.16, 1, 0.3, 1],\r\n onUpdate: (value) => {\r\n element.style.setProperty(\"--start\", String(value));\r\n },\r\n });\r\n });\r\n}\r\n</script>\r\n"
10
+ },
11
+ {
12
+ "path": "index.ts",
13
+ "content": "export { default as GlowingEffect } from \"./GlowingEffect.vue\";\r\n"
14
+ }
15
+ ],
16
+ "fileCount": 2,
17
+ "contentHash": "74c546136a675f582285031940676c2921c0829b"
18
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "gradient-button",
3
+ "dependencies": [],
4
+ "files": [
5
+ {
6
+ "path": "GradientButton.vue",
7
+ "content": "<template>\r\n <button\r\n :class=\"\r\n cn(\r\n 'relative flex items-center justify-center min-w-28 min-h-10 overflow-hidden before:absolute before:-inset-[200%] animate-rainbow rainbow-btn',\r\n props.class,\r\n )\r\n \"\r\n >\r\n <span class=\"btn-content inline-flex size-full items-center justify-center px-4 py-2\">\r\n <slot />\r\n </span>\r\n </button>\r\n</template>\r\n\r\n<script lang=\"ts\" setup>\r\nimport { cn } from \"@/lib/utils\";\r\nimport { computed } from \"vue\";\r\n\r\ninterface GradientButtonProps {\r\n borderWidth?: number;\r\n colors?: string[];\r\n duration?: number;\r\n borderRadius?: number;\r\n blur?: number;\r\n class?: string;\r\n bgColor?: string;\r\n}\r\n\r\nconst props = withDefaults(defineProps<GradientButtonProps>(), {\r\n colors: () => [\r\n \"#FF0000\",\r\n \"#FFA500\",\r\n \"#FFFF00\",\r\n \"#008000\",\r\n \"#0000FF\",\r\n \"#4B0082\",\r\n \"#EE82EE\",\r\n \"#FF0000\",\r\n ],\r\n duration: 2500,\r\n borderWidth: 2,\r\n borderRadius: 8,\r\n blur: 4,\r\n bgColor: \"#000\",\r\n});\r\n\r\nconst durationInMilliseconds = computed(() => `${props.duration}ms`);\r\nconst allColors = computed(() => props.colors.join(\", \"));\r\nconst borderWidthInPx = computed(() => `${props.borderWidth}px`);\r\nconst borderRadiusInPx = computed(() => `${props.borderRadius}px`);\r\nconst blurPx = computed(() => `${props.blur}px`);\r\n</script>\r\n\r\n<style scoped>\r\n.animate-rainbow::before {\r\n content: \"\";\r\n background: conic-gradient(v-bind(allColors));\r\n animation: rotate-rainbow v-bind(durationInMilliseconds) linear infinite;\r\n filter: blur(v-bind(blurPx));\r\n padding: v-bind(borderWidthInPx);\r\n}\r\n\r\n.rainbow-btn {\r\n padding: v-bind(borderWidthInPx);\r\n border-radius: v-bind(borderRadiusInPx);\r\n}\r\n\r\n.btn-content {\r\n border-radius: v-bind(borderRadiusInPx);\r\n background-color: v-bind(bgColor);\r\n z-index: 0;\r\n}\r\n\r\n@keyframes rotate-rainbow {\r\n 0% {\r\n transform: rotate(0deg);\r\n }\r\n 100% {\r\n transform: rotate(360deg);\r\n }\r\n}\r\n</style>\r\n"
8
+ },
9
+ {
10
+ "path": "index.ts",
11
+ "content": "export { default as GradientButton } from \"./GradientButton.vue\";\r\n"
12
+ }
13
+ ],
14
+ "fileCount": 2,
15
+ "contentHash": "eb66ae314cdce88201c86cad34baa20f712bc154"
16
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "halo-search",
3
+ "dependencies": [],
4
+ "files": [
5
+ {
6
+ "path": "HaloSearch.vue",
7
+ "content": "<template>\r\n <div\r\n id=\"halo-search\"\r\n :class=\"props.class\"\r\n >\r\n <div class=\"aurora-glow\"></div>\r\n <div class=\"outer-ring\"></div>\r\n <div class=\"outer-ring\"></div>\r\n <div class=\"outer-ring\"></div>\r\n\r\n <div class=\"inner-glow\"></div>\r\n\r\n <div class=\"main-border\"></div>\r\n\r\n <div id=\"search-wrapper\">\r\n <input\r\n placeholder=\"Search...\"\r\n type=\"text\"\r\n name=\"text\"\r\n class=\"search-field\"\r\n />\r\n <div id=\"text-mask\"></div>\r\n <div class=\"search-btn-border\"></div>\r\n <span\r\n :class=\"[\r\n 'absolute top-2 right-2 flex items-center justify-center z-[2] max-h-10 max-w-10 size-full isolate overflow-hidden rounded-lg border border-transparent border-solid',\r\n ]\"\r\n style=\"background: linear-gradient(180deg, #161329, black, #1d1b4b)\"\r\n >\r\n <Icon\r\n name=\"lucide:search\"\r\n size=\"24\"\r\n />\r\n </span>\r\n </div>\r\n </div>\r\n</template>\r\n\r\n<script lang=\"ts\" setup>\r\nimport type { HTMLAttributes } from \"vue\";\r\n\r\ninterface Props {\r\n class?: HTMLAttributes[\"class\"];\r\n}\r\n\r\nconst props = defineProps<Props>();\r\n</script>\r\n\r\n<style scoped>\r\n#halo-search {\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n}\r\n\r\n#search-wrapper {\r\n position: relative;\r\n}\r\n\r\n.grid {\r\n height: 800px;\r\n width: 800px;\r\n background-image:\r\n linear-gradient(to right, #0f0f10 1px, transparent 1px),\r\n linear-gradient(to bottom, #0f0f10 1px, transparent 1px);\r\n background-size: 1rem 1rem;\r\n background-position: center center;\r\n position: absolute;\r\n z-index: -1;\r\n filter: blur(1px);\r\n}\r\n\r\n.search-field {\r\n background-color: #010201;\r\n border: none;\r\n width: 301px;\r\n height: 56px;\r\n border-radius: 10px;\r\n color: white;\r\n padding-right: 60px;\r\n padding-left: 16px;\r\n font-size: 18px;\r\n}\r\n\r\n.search-field::placeholder {\r\n color: #c0b9c0;\r\n}\r\n\r\n.search-field:focus {\r\n outline: none;\r\n}\r\n\r\n.inner-glow,\r\n.main-border,\r\n.outer-ring,\r\n.aurora-glow {\r\n max-height: 70px;\r\n max-width: 314px;\r\n height: 100%;\r\n width: 100%;\r\n position: absolute;\r\n overflow: hidden;\r\n z-index: -1;\r\n border-radius: 12px;\r\n filter: blur(3px);\r\n}\r\n\r\n.inner-glow {\r\n max-height: 63px;\r\n max-width: 307px;\r\n border-radius: 10px;\r\n filter: blur(2px);\r\n}\r\n\r\n.inner-glow::before {\r\n content: \"\";\r\n z-index: -2;\r\n text-align: center;\r\n top: 50%;\r\n left: 50%;\r\n transform: translate(-50%, -50%) rotate(83deg);\r\n position: absolute;\r\n width: 600px;\r\n height: 600px;\r\n background-repeat: no-repeat;\r\n background-position: 0 0;\r\n filter: brightness(1.4);\r\n background-image: conic-gradient(\r\n rgba(0, 0, 0, 0) 0%,\r\n #a099d8,\r\n rgba(0, 0, 0, 0) 8%,\r\n rgba(0, 0, 0, 0) 50%,\r\n #dfa2da,\r\n rgba(0, 0, 0, 0) 58%\r\n );\r\n transition: all 2s;\r\n}\r\n\r\n.main-border {\r\n max-height: 59px;\r\n max-width: 303px;\r\n border-radius: 11px;\r\n filter: blur(0.5px);\r\n}\r\n\r\n.main-border::before {\r\n content: \"\";\r\n z-index: -2;\r\n text-align: center;\r\n top: 50%;\r\n left: 50%;\r\n transform: translate(-50%, -50%) rotate(70deg);\r\n position: absolute;\r\n width: 600px;\r\n height: 600px;\r\n filter: brightness(1.3);\r\n background-repeat: no-repeat;\r\n background-position: 0 0;\r\n background-image: conic-gradient(\r\n #1c191c,\r\n #402fb5 5%,\r\n #1c191c 14%,\r\n #1c191c 50%,\r\n #cf30aa 60%,\r\n #1c191c 64%\r\n );\r\n transition: all 2s;\r\n}\r\n\r\n.outer-ring {\r\n max-height: 65px;\r\n max-width: 312px;\r\n}\r\n\r\n.outer-ring::before {\r\n content: \"\";\r\n z-index: -2;\r\n text-align: center;\r\n top: 50%;\r\n left: 50%;\r\n transform: translate(-50%, -50%) rotate(82deg);\r\n position: absolute;\r\n width: 600px;\r\n height: 600px;\r\n background-repeat: no-repeat;\r\n background-position: 0 0;\r\n background-image: conic-gradient(\r\n rgba(0, 0, 0, 0),\r\n #18116a,\r\n rgba(0, 0, 0, 0) 10%,\r\n rgba(0, 0, 0, 0) 50%,\r\n #6e1b60,\r\n rgba(0, 0, 0, 0) 60%\r\n );\r\n transition: all 2s;\r\n}\r\n\r\n.aurora-glow {\r\n overflow: hidden;\r\n filter: blur(30px);\r\n opacity: 0.4;\r\n max-height: 130px;\r\n max-width: 354px;\r\n}\r\n\r\n.aurora-glow:before {\r\n content: \"\";\r\n z-index: -2;\r\n text-align: center;\r\n top: 50%;\r\n left: 50%;\r\n transform: translate(-50%, -50%) rotate(60deg);\r\n position: absolute;\r\n width: 999px;\r\n height: 999px;\r\n background-repeat: no-repeat;\r\n background-position: 0 0;\r\n background-image: conic-gradient(#000, #402fb5 5%, #000 38%, #000 50%, #cf30aa 60%, #000 87%);\r\n transition: all 2s;\r\n}\r\n\r\n/* ===========================================\r\n INPUT OVERLAY & ACCENT EFFECTS\r\n =========================================== */\r\n\r\n#text-mask {\r\n pointer-events: none;\r\n width: 100px;\r\n height: 20px;\r\n position: absolute;\r\n background: linear-gradient(90deg, transparent, black);\r\n top: 18px;\r\n left: 32px;\r\n}\r\n\r\n#accent-blur {\r\n pointer-events: none;\r\n width: 30px;\r\n height: 20px;\r\n position: absolute;\r\n background: #cf30aa;\r\n top: 10px;\r\n left: 5px;\r\n filter: blur(20px);\r\n opacity: 0.8;\r\n transition: all 2s;\r\n}\r\n\r\n.search-btn-border {\r\n height: 42px;\r\n width: 42px;\r\n position: absolute;\r\n overflow: hidden;\r\n top: 7px;\r\n right: 7px;\r\n border-radius: 12px;\r\n}\r\n\r\n.search-btn-border::before {\r\n content: \"\";\r\n text-align: center;\r\n top: 50%;\r\n left: 50%;\r\n transform: translate(-50%, -50%) rotate(90deg);\r\n position: absolute;\r\n width: 600px;\r\n height: 600px;\r\n background-repeat: no-repeat;\r\n background-position: 0 0;\r\n filter: brightness(1.35);\r\n background-image: conic-gradient(\r\n rgba(0, 0, 0, 0),\r\n #3d3a4f,\r\n rgba(0, 0, 0, 0) 50%,\r\n rgba(0, 0, 0, 0) 50%,\r\n #3d3a4f,\r\n rgba(0, 0, 0, 0) 100%\r\n );\r\n animation: rotate 4s linear infinite;\r\n}\r\n\r\n#halo-search:hover > .outer-ring::before {\r\n transform: translate(-50%, -50%) rotate(-98deg);\r\n}\r\n\r\n#halo-search:hover > .aurora-glow::before {\r\n transform: translate(-50%, -50%) rotate(-120deg);\r\n}\r\n\r\n#halo-search:hover > .inner-glow::before {\r\n transform: translate(-50%, -50%) rotate(-97deg);\r\n}\r\n\r\n#halo-search:hover > .main-border::before {\r\n transform: translate(-50%, -50%) rotate(-110deg);\r\n}\r\n\r\n#search-wrapper:hover > #accent-blur {\r\n opacity: 0;\r\n}\r\n\r\n#halo-search:focus-within > .outer-ring::before {\r\n transform: translate(-50%, -50%) rotate(442deg);\r\n transition: all 4s;\r\n}\r\n\r\n#halo-search:focus-within > .aurora-glow::before {\r\n transform: translate(-50%, -50%) rotate(420deg);\r\n transition: all 4s;\r\n}\r\n\r\n#halo-search:focus-within > .inner-glow::before {\r\n transform: translate(-50%, -50%) rotate(443deg);\r\n transition: all 4s;\r\n}\r\n\r\n#halo-search:focus-within > .main-border::before {\r\n transform: translate(-50%, -50%) rotate(430deg);\r\n transition: all 4s;\r\n}\r\n\r\n#search-wrapper:focus-within > #text-mask {\r\n display: none;\r\n}\r\n\r\n@keyframes rotate {\r\n 100% {\r\n transform: translate(-50%, -50%) rotate(450deg);\r\n }\r\n}\r\n\r\n@keyframes leftright {\r\n 0% {\r\n transform: translate(0px, 0px);\r\n opacity: 1;\r\n }\r\n 49% {\r\n transform: translate(250px, 0px);\r\n opacity: 0;\r\n }\r\n 80% {\r\n transform: translate(-40px, 0px);\r\n opacity: 0;\r\n }\r\n 100% {\r\n transform: translate(0px, 0px);\r\n opacity: 1;\r\n }\r\n}\r\n</style>\r\n"
8
+ },
9
+ {
10
+ "path": "index.ts",
11
+ "content": "export { default as HaloSearch } from \"./HaloSearch.vue\";\r\n"
12
+ }
13
+ ],
14
+ "fileCount": 2,
15
+ "contentHash": "e6a38b3bd0cb2069ef8eea242f2e0ae14cfcc48c"
16
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "hyper-text",
3
+ "dependencies": [
4
+ "@vueuse/core",
5
+ "motion-v"
6
+ ],
7
+ "files": [
8
+ {
9
+ "path": "HyperText.vue",
10
+ "content": "<template>\r\n <div\r\n :class=\"cn('flex scale-100 cursor-default overflow-hidden py-2', $props.class)\"\r\n @mouseenter=\"triggerAnimation\"\r\n >\r\n <div class=\"flex\">\r\n <Motion\r\n v-for=\"(letter, i) in displayText\"\r\n :key=\"i\"\r\n as=\"span\"\r\n :class=\"cn(letter === ' ' ? 'w-3' : '', $props.class)\"\r\n class=\"inline-block font-mono\"\r\n :initial=\"{ opacity: 0, y: -10 }\"\r\n :animate=\"{ opacity: 1, y: 0 }\"\r\n :delay=\"i * (duration / (text.length * 10))\"\r\n >\r\n {{ letter.toUpperCase() }}\r\n </Motion>\r\n </div>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { ref, computed, watch, type HTMLAttributes } from \"vue\";\r\nimport { useIntervalFn } from \"@vueuse/core\";\r\nimport { cn } from \"@/lib/utils\";\r\nimport { Motion } from \"motion-v\";\r\n\r\nconst props = withDefaults(\r\n defineProps<{\r\n class?: HTMLAttributes[\"class\"];\r\n text: string;\r\n duration?: number;\r\n animateOnLoad: boolean;\r\n }>(),\r\n {\r\n duration: 800,\r\n },\r\n);\r\n\r\nconst alphabets = \"ABCDEFGHIJKLMNOPQRSTUVWXYZ\";\r\nconst displayText = ref(props.text.split(\"\"));\r\nconst iterations = ref(0);\r\n\r\nfunction getRandomLetter() {\r\n return alphabets[Math.floor(Math.random() * alphabets.length)];\r\n}\r\nfunction triggerAnimation() {\r\n iterations.value = 0;\r\n startAnimation();\r\n}\r\n\r\nconst { pause, resume } = useIntervalFn(\r\n () => {\r\n if (iterations.value < props.text.length) {\r\n displayText.value = displayText.value.map((l, i) =>\r\n l === \" \" ? l : i <= iterations.value ? props.text[i] : getRandomLetter(),\r\n );\r\n iterations.value += 0.1;\r\n } else {\r\n pause();\r\n }\r\n },\r\n computed(() => props.duration / (props.text.length * 10)),\r\n);\r\n\r\nfunction startAnimation() {\r\n pause();\r\n resume();\r\n}\r\n\r\nwatch(\r\n () => props.text,\r\n (newText) => {\r\n displayText.value = newText.split(\"\");\r\n triggerAnimation();\r\n },\r\n);\r\n\r\nif (props.animateOnLoad) {\r\n triggerAnimation();\r\n}\r\n</script>\r\n"
11
+ },
12
+ {
13
+ "path": "index.ts",
14
+ "content": "export { default as HyperText } from \"./HyperText.vue\";\r\n"
15
+ }
16
+ ],
17
+ "fileCount": 2,
18
+ "contentHash": "32b41dfca073ba08ef9de303b84566048875e70e"
19
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "icon-cloud",
3
+ "dependencies": [],
4
+ "files": [
5
+ {
6
+ "path": "IconCloud.vue",
7
+ "content": "<template>\r\n <canvas\r\n ref=\"canvasRef\"\r\n width=\"300\"\r\n height=\"300\"\r\n :class=\"cn('rounded-lg', $props.class)\"\r\n role=\"img\"\r\n aria-label=\"Interactive 3D Image Cloud\"\r\n @mousedown=\"handleMouseDown\"\r\n @mousemove=\"handleMouseMove\"\r\n @mouseup=\"handleMouseUp\"\r\n @mouseleave=\"handleMouseUp\"\r\n />\r\n</template>\r\n\r\n<script lang=\"ts\" setup>\r\nimport type { SphereIcon, IconCloudProps } from \"./index\";\r\nimport { cn } from \"@/lib/utils\";\r\nimport { ref, onMounted, onBeforeUnmount, reactive, watchEffect } from \"vue\";\r\n\r\nconst props = defineProps<IconCloudProps>();\r\n\r\nconst { images } = props;\r\n\r\nconst canvasRef = ref<HTMLCanvasElement | null>(null);\r\nconst animationFrameRef = ref<number>(0);\r\n\r\nconst imageCanvasesRef = ref<HTMLCanvasElement[]>([]);\r\nconst imagesLoadedRef = ref<boolean[]>([]);\r\n\r\nconst imagePositions = ref<SphereIcon[]>([]);\r\n\r\nconst rotation = reactive({ x: 0, y: 0 });\r\nconst isDragging = ref(false);\r\nconst lastMousePos = reactive({ x: 0, y: 0 });\r\nconst mousePos = reactive({ x: 0, y: 0 });\r\n\r\nconst targetRotation = ref<{\r\n x: number;\r\n y: number;\r\n startX: number;\r\n startY: number;\r\n distance: number;\r\n startTime: number;\r\n duration: number;\r\n} | null>(null);\r\n\r\nfunction easeOutCubic(t: number): number {\r\n return 1 - (1 - t) ** 3;\r\n}\r\n\r\nwatchEffect(() => {\r\n if (!images) return;\r\n imagesLoadedRef.value = new Array(images.length).fill(false);\r\n\r\n const newImageCanvases = images.map((url, idx) => {\r\n const offscreen = document.createElement(\"canvas\");\r\n offscreen.width = 40;\r\n offscreen.height = 40;\r\n const offCtx = offscreen.getContext(\"2d\");\r\n if (!offCtx) return offscreen;\r\n\r\n const img = new Image();\r\n img.crossOrigin = \"anonymous\";\r\n img.src = url;\r\n img.onload = () => {\r\n offCtx.clearRect(0, 0, offscreen.width, offscreen.height);\r\n\r\n // circular clipping\r\n offCtx.beginPath();\r\n offCtx.arc(20, 20, 20, 0, Math.PI * 2);\r\n offCtx.closePath();\r\n offCtx.clip();\r\n\r\n // draw the image\r\n offCtx.drawImage(img, 0, 0, 40, 40);\r\n imagesLoadedRef.value[idx] = true;\r\n };\r\n\r\n return offscreen;\r\n });\r\n\r\n imageCanvasesRef.value = newImageCanvases;\r\n});\r\n\r\nwatchEffect(() => {\r\n const count = images?.length || 0;\r\n if (count === 0) {\r\n imagePositions.value = [];\r\n return;\r\n }\r\n\r\n const newPositions: SphereIcon[] = [];\r\n const offset = 2 / count;\r\n const increment = Math.PI * (3 - Math.sqrt(5)); // ~2.3999632\r\n\r\n for (let i = 0; i < count; i++) {\r\n const y = i * offset - 1 + offset / 2;\r\n const r = Math.sqrt(1 - y * y);\r\n const phi = i * increment;\r\n const x = Math.cos(phi) * r;\r\n const z = Math.sin(phi) * r;\r\n\r\n newPositions.push({\r\n x: x * 100,\r\n y: y * 100,\r\n z: z * 100,\r\n scale: 1,\r\n opacity: 1,\r\n id: i,\r\n });\r\n }\r\n imagePositions.value = newPositions;\r\n});\r\n\r\nfunction handleMouseDown(e: MouseEvent) {\r\n const canvas = canvasRef.value;\r\n if (!canvas) return;\r\n const rect = canvas.getBoundingClientRect();\r\n const x = e.clientX - rect.left;\r\n const y = e.clientY - rect.top;\r\n const ctx = canvas.getContext(\"2d\");\r\n if (!ctx) return;\r\n\r\n imagePositions.value.forEach((icon) => {\r\n const cosX = Math.cos(rotation.x);\r\n const sinX = Math.sin(rotation.x);\r\n const cosY = Math.cos(rotation.y);\r\n const sinY = Math.sin(rotation.y);\r\n\r\n const rotatedX = icon.x * cosY - icon.z * sinY;\r\n const rotatedZ = icon.x * sinY + icon.z * cosY;\r\n const rotatedY = icon.y * cosX + rotatedZ * sinX;\r\n\r\n const screenX = canvas.width / 2 + rotatedX;\r\n const screenY = canvas.height / 2 + rotatedY;\r\n\r\n const scale = (rotatedZ + 200) / 300;\r\n const radius = 20 * scale;\r\n const dx = x - screenX;\r\n const dy = y - screenY;\r\n\r\n if (dx * dx + dy * dy < radius * radius) {\r\n const targetX = -Math.atan2(icon.y, Math.sqrt(icon.x * icon.x + icon.z * icon.z));\r\n const targetY = Math.atan2(icon.x, icon.z);\r\n const currentX = rotation.x;\r\n const currentY = rotation.y;\r\n const distance = Math.sqrt((targetX - currentX) ** 2 + (targetY - currentY) ** 2);\r\n\r\n const duration = Math.min(2000, Math.max(800, distance * 1000));\r\n targetRotation.value = {\r\n x: targetX,\r\n y: targetY,\r\n startX: currentX,\r\n startY: currentY,\r\n distance,\r\n startTime: performance.now(),\r\n duration,\r\n };\r\n return;\r\n }\r\n });\r\n\r\n isDragging.value = true;\r\n lastMousePos.x = e.clientX;\r\n lastMousePos.y = e.clientY;\r\n}\r\n\r\nfunction handleMouseMove(e: MouseEvent) {\r\n const canvas = canvasRef.value;\r\n if (!canvas) return;\r\n const rect = canvas.getBoundingClientRect();\r\n mousePos.x = e.clientX - rect.left;\r\n mousePos.y = e.clientY - rect.top;\r\n\r\n if (isDragging.value) {\r\n const deltaX = e.clientX - lastMousePos.x;\r\n const deltaY = e.clientY - lastMousePos.y;\r\n\r\n rotation.x += deltaY * 0.002;\r\n rotation.y += deltaX * 0.002;\r\n\r\n lastMousePos.x = e.clientX;\r\n lastMousePos.y = e.clientY;\r\n }\r\n}\r\n\r\nfunction handleMouseUp() {\r\n isDragging.value = false;\r\n}\r\n\r\nonMounted(() => {\r\n const canvas = canvasRef.value;\r\n if (!canvas) return;\r\n const ctx = canvas.getContext(\"2d\");\r\n if (!ctx) return;\r\n\r\n function animate() {\r\n ctx.clearRect(0, 0, canvas.width, canvas.height);\r\n\r\n const centerX = canvas.width / 2;\r\n const centerY = canvas.height / 2;\r\n const dx = mousePos.x - centerX;\r\n const dy = mousePos.y - centerY;\r\n const maxDistance = Math.sqrt(centerX * centerX + centerY * centerY);\r\n const distance = Math.sqrt(dx * dx + dy * dy);\r\n\r\n const speed = 0.003 + (distance / maxDistance) * 0.01;\r\n\r\n if (targetRotation.value) {\r\n const { startX, startY, x: tx, y: ty, startTime, duration } = targetRotation.value;\r\n const elapsed = performance.now() - startTime;\r\n const progress = Math.min(1, elapsed / duration);\r\n const eased = easeOutCubic(progress);\r\n\r\n rotation.x = startX + (tx - startX) * eased;\r\n rotation.y = startY + (ty - startY) * eased;\r\n\r\n if (progress >= 1) {\r\n targetRotation.value = null;\r\n }\r\n } else if (!isDragging.value) {\r\n rotation.x += (dy / canvas.height) * speed;\r\n rotation.y += (dx / canvas.width) * speed;\r\n }\r\n\r\n imagePositions.value.forEach((icon, index) => {\r\n const cosX = Math.cos(rotation.x);\r\n const sinX = Math.sin(rotation.x);\r\n const cosY = Math.cos(rotation.y);\r\n const sinY = Math.sin(rotation.y);\r\n\r\n const rotatedX = icon.x * cosY - icon.z * sinY;\r\n const rotatedZ = icon.x * sinY + icon.z * cosY;\r\n const rotatedY = icon.y * cosX + rotatedZ * sinX;\r\n\r\n const scale = (rotatedZ + 200) / 300;\r\n const opacity = Math.max(0.2, Math.min(1, (rotatedZ + 150) / 200));\r\n\r\n ctx.save();\r\n ctx.translate(centerX + rotatedX, centerY + rotatedY);\r\n ctx.scale(scale, scale);\r\n ctx.globalAlpha = opacity;\r\n\r\n if (imageCanvasesRef.value[index] && imagesLoadedRef.value[index]) {\r\n ctx.drawImage(imageCanvasesRef.value[index], -20, -20, 40, 40);\r\n }\r\n ctx.restore();\r\n });\r\n\r\n animationFrameRef.value = requestAnimationFrame(animate);\r\n }\r\n\r\n animationFrameRef.value = requestAnimationFrame(animate);\r\n});\r\n\r\nonBeforeUnmount(() => {\r\n if (animationFrameRef.value) {\r\n cancelAnimationFrame(animationFrameRef.value);\r\n }\r\n});\r\n</script>\r\n"
8
+ },
9
+ {
10
+ "path": "index.ts",
11
+ "content": "import type { HTMLAttributes } from \"vue\";\r\n\r\nexport interface SphereIcon {\r\n x: number;\r\n y: number;\r\n z: number;\r\n scale: number;\r\n opacity: number;\r\n id: number;\r\n}\r\n\r\nexport interface IconCloudProps {\r\n class?: HTMLAttributes[\"class\"];\r\n images?: string[];\r\n}\r\n\r\nexport { default as IconCloud } from \"./IconCloud.vue\";\r\n"
12
+ }
13
+ ],
14
+ "fileCount": 2,
15
+ "contentHash": "ece17c9754de38ba8cb100be6946983731e68fd2"
16
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "image-trail-cursor",
3
+ "dependencies": [
4
+ "gsap"
5
+ ],
6
+ "files": [
7
+ {
8
+ "path": "ImageTrailCursor.vue",
9
+ "content": "<template>\r\n <div\r\n ref=\"containerRef\"\r\n class=\"w-full h-full relative z-[100] rounded-lg bg-transparent overflow-visible\"\r\n >\r\n <div\r\n v-for=\"(image, i) in images\"\r\n :key=\"variant + i\"\r\n class=\"content__img w-[190px] aspect-[1.1] rounded-[15px] absolute top-0 left-0 opacity-0 overflow-hidden [will-change:transform,filter]\"\r\n >\r\n <div\r\n class=\"content__img-inner bg-center bg-cover w-[calc(100%+20px)] h-[calc(100%+20px)] absolute top-[-10px] left-[-10px]\"\r\n :style=\"`background-image: url(${image})`\"\r\n />\r\n </div>\r\n </div>\r\n</template>\r\n\r\n<script lang=\"ts\" setup>\r\nimport { variantMap } from \"./trail-variants\";\r\n\r\nexport type VariantType = \"type1\" | \"type2\" | \"type3\" | \"type4\" | \"type5\" | \"type6\" | \"type7\";\r\n\r\ninterface Props {\r\n images?: string[];\r\n variant?: VariantType;\r\n}\r\n\r\nconst { images = [], variant = \"type1\" } = defineProps<Props>();\r\n\r\nconst containerRef = useTemplateRef(\"containerRef\");\r\n\r\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\r\nlet currentInstance: any = null;\r\n\r\n// Initialize the variant instance\r\nfunction initializeVariant() {\r\n if (!containerRef.value) return;\r\n\r\n if (currentInstance && typeof currentInstance.destroy === \"function\") {\r\n currentInstance.destroy();\r\n }\r\n\r\n // Get the appropriate variant class constructor\r\n const Variant = variantMap[variant] || variantMap.type1;\r\n\r\n // Create a new instance with the container element\r\n currentInstance = new Variant!(containerRef.value);\r\n}\r\n\r\n// Initialize on mount\r\nonMounted(() => {\r\n initializeVariant();\r\n});\r\n\r\n// Re-initialize when variant prop changes\r\nwatch(\r\n () => variant,\r\n () => {\r\n initializeVariant();\r\n },\r\n);\r\n\r\n// Clean up on component unmount\r\nonBeforeUnmount(() => {\r\n if (currentInstance && typeof currentInstance.destroy === \"function\") {\r\n currentInstance.destroy();\r\n }\r\n});\r\n</script>\r\n"
10
+ },
11
+ {
12
+ "path": "index.ts",
13
+ "content": "export { default as ImageTrailCursor } from \"./ImageTrailCursor.vue\";\r\nexport * from \"./trail-variants\";\r\n"
14
+ },
15
+ {
16
+ "path": "trail-variants.ts",
17
+ "content": "import { gsap } from \"gsap\";\r\n\r\nexport class ImageTrailVariant1 {\r\n private container: HTMLDivElement;\r\n private DOM: { el: HTMLDivElement };\r\n private images: ImageItem[];\r\n private imagesTotal: number;\r\n private imgPosition: number;\r\n private zIndexVal: number;\r\n private activeImagesCount: number;\r\n private isIdle: boolean;\r\n private threshold: number;\r\n private mousePos: { x: number; y: number };\r\n private lastMousePos: { x: number; y: number };\r\n private cacheMousePos: { x: number; y: number };\r\n\r\n constructor(container: HTMLDivElement) {\r\n this.container = container;\r\n this.DOM = { el: container };\r\n this.images = [...container.querySelectorAll(\".content__img\")].map(\r\n (img) => new ImageItem(img as HTMLDivElement),\r\n );\r\n this.imagesTotal = this.images.length;\r\n this.imgPosition = 0;\r\n this.zIndexVal = 1;\r\n this.activeImagesCount = 0;\r\n this.isIdle = true;\r\n this.threshold = 80;\r\n this.mousePos = { x: 0, y: 0 };\r\n this.lastMousePos = { x: 0, y: 0 };\r\n this.cacheMousePos = { x: 0, y: 0 };\r\n\r\n const handlePointerMove = (ev: MouseEvent | TouchEvent) => {\r\n const rect = this.container.getBoundingClientRect();\r\n this.mousePos = getLocalPointerPos(ev, rect);\r\n };\r\n container.addEventListener(\"mousemove\", handlePointerMove);\r\n container.addEventListener(\"touchmove\", handlePointerMove);\r\n\r\n const initRender = (ev: MouseEvent | TouchEvent) => {\r\n const rect = this.container.getBoundingClientRect();\r\n this.mousePos = getLocalPointerPos(ev, rect);\r\n this.cacheMousePos = { ...this.mousePos };\r\n requestAnimationFrame(() => this.render());\r\n container.removeEventListener(\"mousemove\", initRender as EventListener);\r\n container.removeEventListener(\"touchmove\", initRender as EventListener);\r\n };\r\n container.addEventListener(\"mousemove\", initRender as EventListener);\r\n container.addEventListener(\"touchmove\", initRender as EventListener);\r\n }\r\n\r\n private render() {\r\n const distance = getMouseDistance(this.mousePos, this.lastMousePos);\r\n this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.1);\r\n this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.1);\r\n\r\n if (distance > this.threshold) {\r\n this.showNextImage();\r\n this.lastMousePos = { ...this.mousePos };\r\n }\r\n if (this.isIdle && this.zIndexVal !== 1) {\r\n this.zIndexVal = 1;\r\n }\r\n requestAnimationFrame(() => this.render());\r\n }\r\n\r\n private showNextImage() {\r\n ++this.zIndexVal;\r\n this.imgPosition = this.imgPosition < this.imagesTotal - 1 ? this.imgPosition + 1 : 0;\r\n const img = this.images[this.imgPosition];\r\n\r\n gsap.killTweensOf(img.DOM.el);\r\n gsap\r\n .timeline({\r\n onStart: () => this.onImageActivated(),\r\n onComplete: () => this.onImageDeactivated(),\r\n })\r\n .fromTo(\r\n img.DOM.el,\r\n {\r\n opacity: 1,\r\n scale: 1,\r\n zIndex: this.zIndexVal,\r\n x: this.cacheMousePos.x - (img.rect?.width ?? 0) / 2,\r\n y: this.cacheMousePos.y - (img.rect?.height ?? 0) / 2,\r\n },\r\n {\r\n duration: 0.4,\r\n ease: \"power1\",\r\n x: this.mousePos.x - (img.rect?.width ?? 0) / 2,\r\n y: this.mousePos.y - (img.rect?.height ?? 0) / 2,\r\n },\r\n 0,\r\n )\r\n .to(\r\n img.DOM.el,\r\n {\r\n duration: 0.4,\r\n ease: \"power3\",\r\n opacity: 0,\r\n scale: 0.2,\r\n },\r\n 0.4,\r\n );\r\n }\r\n\r\n private onImageActivated() {\r\n this.activeImagesCount++;\r\n this.isIdle = false;\r\n }\r\n\r\n private onImageDeactivated() {\r\n this.activeImagesCount--;\r\n if (this.activeImagesCount === 0) {\r\n this.isIdle = true;\r\n }\r\n }\r\n}\r\n\r\nexport class ImageTrailVariant2 {\r\n private container: HTMLDivElement;\r\n private DOM: { el: HTMLDivElement };\r\n private images: ImageItem[];\r\n private imagesTotal: number;\r\n private imgPosition: number;\r\n private zIndexVal: number;\r\n private activeImagesCount: number;\r\n private isIdle: boolean;\r\n private threshold: number;\r\n private mousePos: { x: number; y: number };\r\n private lastMousePos: { x: number; y: number };\r\n private cacheMousePos: { x: number; y: number };\r\n\r\n constructor(container: HTMLDivElement) {\r\n this.container = container;\r\n this.DOM = { el: container };\r\n this.images = [...container.querySelectorAll(\".content__img\")].map(\r\n (img) => new ImageItem(img as HTMLDivElement),\r\n );\r\n this.imagesTotal = this.images.length;\r\n this.imgPosition = 0;\r\n this.zIndexVal = 1;\r\n this.activeImagesCount = 0;\r\n this.isIdle = true;\r\n this.threshold = 80;\r\n this.mousePos = { x: 0, y: 0 };\r\n this.lastMousePos = { x: 0, y: 0 };\r\n this.cacheMousePos = { x: 0, y: 0 };\r\n\r\n const handlePointerMove = (ev: MouseEvent | TouchEvent) => {\r\n const rect = container.getBoundingClientRect();\r\n this.mousePos = getLocalPointerPos(ev, rect);\r\n };\r\n container.addEventListener(\"mousemove\", handlePointerMove);\r\n container.addEventListener(\"touchmove\", handlePointerMove);\r\n\r\n const initRender = (ev: MouseEvent | TouchEvent) => {\r\n const rect = container.getBoundingClientRect();\r\n this.mousePos = getLocalPointerPos(ev, rect);\r\n this.cacheMousePos = { ...this.mousePos };\r\n requestAnimationFrame(() => this.render());\r\n container.removeEventListener(\"mousemove\", initRender as EventListener);\r\n container.removeEventListener(\"touchmove\", initRender as EventListener);\r\n };\r\n container.addEventListener(\"mousemove\", initRender as EventListener);\r\n container.addEventListener(\"touchmove\", initRender as EventListener);\r\n }\r\n\r\n private render() {\r\n const distance = getMouseDistance(this.mousePos, this.lastMousePos);\r\n this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.1);\r\n this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.1);\r\n\r\n if (distance > this.threshold) {\r\n this.showNextImage();\r\n this.lastMousePos = { ...this.mousePos };\r\n }\r\n if (this.isIdle && this.zIndexVal !== 1) {\r\n this.zIndexVal = 1;\r\n }\r\n requestAnimationFrame(() => this.render());\r\n }\r\n\r\n private showNextImage() {\r\n ++this.zIndexVal;\r\n this.imgPosition = this.imgPosition < this.imagesTotal - 1 ? this.imgPosition + 1 : 0;\r\n const img = this.images[this.imgPosition];\r\n\r\n gsap.killTweensOf(img.DOM.el);\r\n gsap\r\n .timeline({\r\n onStart: () => this.onImageActivated(),\r\n onComplete: () => this.onImageDeactivated(),\r\n })\r\n .fromTo(\r\n img.DOM.el,\r\n {\r\n opacity: 1,\r\n scale: 0,\r\n zIndex: this.zIndexVal,\r\n x: this.cacheMousePos.x - (img.rect?.width ?? 0) / 2,\r\n y: this.cacheMousePos.y - (img.rect?.height ?? 0) / 2,\r\n },\r\n {\r\n duration: 0.4,\r\n ease: \"power1\",\r\n scale: 1,\r\n x: this.mousePos.x - (img.rect?.width ?? 0) / 2,\r\n y: this.mousePos.y - (img.rect?.height ?? 0) / 2,\r\n },\r\n 0,\r\n )\r\n .fromTo(\r\n img.DOM.inner,\r\n { scale: 2.8, filter: \"brightness(250%)\" },\r\n {\r\n duration: 0.4,\r\n ease: \"power1\",\r\n scale: 1,\r\n filter: \"brightness(100%)\",\r\n },\r\n 0,\r\n )\r\n .to(\r\n img.DOM.el,\r\n {\r\n duration: 0.4,\r\n ease: \"power2\",\r\n opacity: 0,\r\n scale: 0.2,\r\n },\r\n 0.45,\r\n );\r\n }\r\n\r\n private onImageActivated() {\r\n this.activeImagesCount++;\r\n this.isIdle = false;\r\n }\r\n\r\n private onImageDeactivated() {\r\n this.activeImagesCount--;\r\n if (this.activeImagesCount === 0) {\r\n this.isIdle = true;\r\n }\r\n }\r\n}\r\n\r\nexport class ImageTrailVariant3 {\r\n private container: HTMLDivElement;\r\n private DOM: { el: HTMLDivElement };\r\n private images: ImageItem[];\r\n private imagesTotal: number;\r\n private imgPosition: number;\r\n private zIndexVal: number;\r\n private activeImagesCount: number;\r\n private isIdle: boolean;\r\n private threshold: number;\r\n private mousePos: { x: number; y: number };\r\n private lastMousePos: { x: number; y: number };\r\n private cacheMousePos: { x: number; y: number };\r\n\r\n constructor(container: HTMLDivElement) {\r\n this.container = container;\r\n this.DOM = { el: container };\r\n this.images = [...container.querySelectorAll(\".content__img\")].map(\r\n (img) => new ImageItem(img as HTMLDivElement),\r\n );\r\n this.imagesTotal = this.images.length;\r\n this.imgPosition = 0;\r\n this.zIndexVal = 1;\r\n this.activeImagesCount = 0;\r\n this.isIdle = true;\r\n this.threshold = 80;\r\n this.mousePos = { x: 0, y: 0 };\r\n this.lastMousePos = { x: 0, y: 0 };\r\n this.cacheMousePos = { x: 0, y: 0 };\r\n\r\n const handlePointerMove = (ev: MouseEvent | TouchEvent) => {\r\n const rect = container.getBoundingClientRect();\r\n this.mousePos = getLocalPointerPos(ev, rect);\r\n };\r\n container.addEventListener(\"mousemove\", handlePointerMove);\r\n container.addEventListener(\"touchmove\", handlePointerMove);\r\n\r\n const initRender = (ev: MouseEvent | TouchEvent) => {\r\n const rect = container.getBoundingClientRect();\r\n this.mousePos = getLocalPointerPos(ev, rect);\r\n this.cacheMousePos = { ...this.mousePos };\r\n requestAnimationFrame(() => this.render());\r\n container.removeEventListener(\"mousemove\", initRender as EventListener);\r\n container.removeEventListener(\"touchmove\", initRender as EventListener);\r\n };\r\n container.addEventListener(\"mousemove\", initRender as EventListener);\r\n container.addEventListener(\"touchmove\", initRender as EventListener);\r\n }\r\n\r\n private render() {\r\n const distance = getMouseDistance(this.mousePos, this.lastMousePos);\r\n this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.1);\r\n this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.1);\r\n\r\n if (distance > this.threshold) {\r\n this.showNextImage();\r\n this.lastMousePos = { ...this.mousePos };\r\n }\r\n if (this.isIdle && this.zIndexVal !== 1) {\r\n this.zIndexVal = 1;\r\n }\r\n requestAnimationFrame(() => this.render());\r\n }\r\n\r\n private showNextImage() {\r\n ++this.zIndexVal;\r\n this.imgPosition = this.imgPosition < this.imagesTotal - 1 ? this.imgPosition + 1 : 0;\r\n const img = this.images[this.imgPosition];\r\n\r\n gsap.killTweensOf(img.DOM.el);\r\n gsap\r\n .timeline({\r\n onStart: () => this.onImageActivated(),\r\n onComplete: () => this.onImageDeactivated(),\r\n })\r\n .fromTo(\r\n img.DOM.el,\r\n {\r\n opacity: 1,\r\n scale: 0,\r\n zIndex: this.zIndexVal,\r\n xPercent: 0,\r\n yPercent: 0,\r\n x: this.cacheMousePos.x - (img.rect?.width ?? 0) / 2,\r\n y: this.cacheMousePos.y - (img.rect?.height ?? 0) / 2,\r\n },\r\n {\r\n duration: 0.4,\r\n ease: \"power1\",\r\n scale: 1,\r\n x: this.mousePos.x - (img.rect?.width ?? 0) / 2,\r\n y: this.mousePos.y - (img.rect?.height ?? 0) / 2,\r\n },\r\n 0,\r\n )\r\n .fromTo(\r\n img.DOM.inner,\r\n { scale: 1.2 },\r\n {\r\n duration: 0.4,\r\n ease: \"power1\",\r\n scale: 1,\r\n },\r\n 0,\r\n )\r\n .to(\r\n img.DOM.el,\r\n {\r\n duration: 0.6,\r\n ease: \"power2\",\r\n opacity: 0,\r\n scale: 0.2,\r\n xPercent: () => gsap.utils.random(-30, 30),\r\n yPercent: -200,\r\n },\r\n 0.6,\r\n );\r\n }\r\n\r\n private onImageActivated() {\r\n this.activeImagesCount++;\r\n this.isIdle = false;\r\n }\r\n\r\n private onImageDeactivated() {\r\n this.activeImagesCount--;\r\n if (this.activeImagesCount === 0) {\r\n this.isIdle = true;\r\n }\r\n }\r\n}\r\n\r\nexport class ImageTrailVariant4 {\r\n private container: HTMLDivElement;\r\n private DOM: { el: HTMLDivElement };\r\n private images: ImageItem[];\r\n private imagesTotal: number;\r\n private imgPosition: number;\r\n private zIndexVal: number;\r\n private activeImagesCount: number;\r\n private isIdle: boolean;\r\n private threshold: number;\r\n private mousePos: { x: number; y: number };\r\n private lastMousePos: { x: number; y: number };\r\n private cacheMousePos: { x: number; y: number };\r\n\r\n constructor(container: HTMLDivElement) {\r\n this.container = container;\r\n this.DOM = { el: container };\r\n this.images = [...container.querySelectorAll(\".content__img\")].map(\r\n (img) => new ImageItem(img as HTMLDivElement),\r\n );\r\n this.imagesTotal = this.images.length;\r\n this.imgPosition = 0;\r\n this.zIndexVal = 1;\r\n this.activeImagesCount = 0;\r\n this.isIdle = true;\r\n this.threshold = 80;\r\n this.mousePos = { x: 0, y: 0 };\r\n this.lastMousePos = { x: 0, y: 0 };\r\n this.cacheMousePos = { x: 0, y: 0 };\r\n\r\n const handlePointerMove = (ev: MouseEvent | TouchEvent) => {\r\n const rect = container.getBoundingClientRect();\r\n this.mousePos = getLocalPointerPos(ev, rect);\r\n };\r\n container.addEventListener(\"mousemove\", handlePointerMove);\r\n container.addEventListener(\"touchmove\", handlePointerMove);\r\n\r\n const initRender = (ev: MouseEvent | TouchEvent) => {\r\n const rect = container.getBoundingClientRect();\r\n this.mousePos = getLocalPointerPos(ev, rect);\r\n this.cacheMousePos = { ...this.mousePos };\r\n requestAnimationFrame(() => this.render());\r\n container.removeEventListener(\"mousemove\", initRender as EventListener);\r\n container.removeEventListener(\"touchmove\", initRender as EventListener);\r\n };\r\n container.addEventListener(\"mousemove\", initRender as EventListener);\r\n container.addEventListener(\"touchmove\", initRender as EventListener);\r\n }\r\n\r\n private render() {\r\n const distance = getMouseDistance(this.mousePos, this.lastMousePos);\r\n if (distance > this.threshold) {\r\n this.showNextImage();\r\n this.lastMousePos = { ...this.mousePos };\r\n }\r\n this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.1);\r\n this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.1);\r\n\r\n if (this.isIdle && this.zIndexVal !== 1) this.zIndexVal = 1;\r\n requestAnimationFrame(() => this.render());\r\n }\r\n\r\n private showNextImage() {\r\n ++this.zIndexVal;\r\n this.imgPosition = this.imgPosition < this.imagesTotal - 1 ? this.imgPosition + 1 : 0;\r\n const img = this.images[this.imgPosition];\r\n gsap.killTweensOf(img.DOM.el);\r\n\r\n let dx = this.mousePos.x - this.cacheMousePos.x;\r\n let dy = this.mousePos.y - this.cacheMousePos.y;\r\n const distance = Math.sqrt(dx * dx + dy * dy);\r\n if (distance !== 0) {\r\n dx /= distance;\r\n dy /= distance;\r\n }\r\n dx *= distance / 100;\r\n dy *= distance / 100;\r\n\r\n gsap\r\n .timeline({\r\n onStart: () => this.onImageActivated(),\r\n onComplete: () => this.onImageDeactivated(),\r\n })\r\n .fromTo(\r\n img.DOM.el,\r\n {\r\n opacity: 1,\r\n scale: 0,\r\n zIndex: this.zIndexVal,\r\n x: this.cacheMousePos.x - (img.rect?.width ?? 0) / 2,\r\n y: this.cacheMousePos.y - (img.rect?.height ?? 0) / 2,\r\n },\r\n {\r\n duration: 0.4,\r\n ease: \"power1\",\r\n scale: 1,\r\n x: this.mousePos.x - (img.rect?.width ?? 0) / 2,\r\n y: this.mousePos.y - (img.rect?.height ?? 0) / 2,\r\n },\r\n 0,\r\n )\r\n .fromTo(\r\n img.DOM.inner,\r\n {\r\n scale: 2,\r\n filter: `brightness(${Math.max((400 * distance) / 100, 100)}%) contrast(${Math.max(\r\n (400 * distance) / 100,\r\n 100,\r\n )}%)`,\r\n },\r\n {\r\n duration: 0.4,\r\n ease: \"power1\",\r\n scale: 1,\r\n filter: \"brightness(100%) contrast(100%)\",\r\n },\r\n 0,\r\n )\r\n .to(\r\n img.DOM.el,\r\n {\r\n duration: 0.4,\r\n ease: \"power3\",\r\n opacity: 0,\r\n },\r\n 0.4,\r\n )\r\n .to(\r\n img.DOM.el,\r\n {\r\n duration: 1.5,\r\n ease: \"power4\",\r\n x: `+=${dx * 110}`,\r\n y: `+=${dy * 110}`,\r\n },\r\n 0.05,\r\n );\r\n }\r\n\r\n private onImageActivated() {\r\n this.activeImagesCount++;\r\n this.isIdle = false;\r\n }\r\n\r\n private onImageDeactivated() {\r\n this.activeImagesCount--;\r\n if (this.activeImagesCount === 0) {\r\n this.isIdle = true;\r\n }\r\n }\r\n}\r\n\r\nexport class ImageTrailVariant5 {\r\n private container: HTMLDivElement;\r\n private DOM: { el: HTMLDivElement };\r\n private images: ImageItem[];\r\n private imagesTotal: number;\r\n private imgPosition: number;\r\n private zIndexVal: number;\r\n private activeImagesCount: number;\r\n private isIdle: boolean;\r\n private threshold: number;\r\n private mousePos: { x: number; y: number };\r\n private lastMousePos: { x: number; y: number };\r\n private cacheMousePos: { x: number; y: number };\r\n private lastAngle: number;\r\n\r\n constructor(container: HTMLDivElement) {\r\n this.container = container;\r\n this.DOM = { el: container };\r\n this.images = [...container.querySelectorAll(\".content__img\")].map(\r\n (img) => new ImageItem(img as HTMLDivElement),\r\n );\r\n this.imagesTotal = this.images.length;\r\n this.imgPosition = 0;\r\n this.zIndexVal = 1;\r\n this.activeImagesCount = 0;\r\n this.isIdle = true;\r\n this.threshold = 80;\r\n this.mousePos = { x: 0, y: 0 };\r\n this.lastMousePos = { x: 0, y: 0 };\r\n this.cacheMousePos = { x: 0, y: 0 };\r\n this.lastAngle = 0;\r\n\r\n const handlePointerMove = (ev: MouseEvent | TouchEvent) => {\r\n const rect = container.getBoundingClientRect();\r\n this.mousePos = getLocalPointerPos(ev, rect);\r\n };\r\n container.addEventListener(\"mousemove\", handlePointerMove);\r\n container.addEventListener(\"touchmove\", handlePointerMove);\r\n\r\n const initRender = (ev: MouseEvent | TouchEvent) => {\r\n const rect = container.getBoundingClientRect();\r\n this.mousePos = getLocalPointerPos(ev, rect);\r\n this.cacheMousePos = { ...this.mousePos };\r\n requestAnimationFrame(() => this.render());\r\n container.removeEventListener(\"mousemove\", initRender as EventListener);\r\n container.removeEventListener(\"touchmove\", initRender as EventListener);\r\n };\r\n container.addEventListener(\"mousemove\", initRender as EventListener);\r\n container.addEventListener(\"touchmove\", initRender as EventListener);\r\n }\r\n\r\n private render() {\r\n const distance = getMouseDistance(this.mousePos, this.lastMousePos);\r\n if (distance > this.threshold) {\r\n this.showNextImage();\r\n this.lastMousePos = { ...this.mousePos };\r\n }\r\n this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.1);\r\n this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.1);\r\n if (this.isIdle && this.zIndexVal !== 1) this.zIndexVal = 1;\r\n requestAnimationFrame(() => this.render());\r\n }\r\n\r\n private showNextImage() {\r\n let dx = this.mousePos.x - this.cacheMousePos.x;\r\n let dy = this.mousePos.y - this.cacheMousePos.y;\r\n let angle = Math.atan2(dy, dx) * (180 / Math.PI);\r\n if (angle < 0) angle += 360;\r\n if (angle > 90 && angle <= 270) angle += 180;\r\n const isMovingClockwise = angle >= this.lastAngle;\r\n this.lastAngle = angle;\r\n const startAngle = isMovingClockwise ? angle - 10 : angle + 10;\r\n const distance = Math.sqrt(dx * dx + dy * dy);\r\n if (distance !== 0) {\r\n dx /= distance;\r\n dy /= distance;\r\n }\r\n dx *= distance / 150;\r\n dy *= distance / 150;\r\n\r\n ++this.zIndexVal;\r\n this.imgPosition = this.imgPosition < this.imagesTotal - 1 ? this.imgPosition + 1 : 0;\r\n const img = this.images[this.imgPosition];\r\n gsap.killTweensOf(img.DOM.el);\r\n\r\n gsap\r\n .timeline({\r\n onStart: () => this.onImageActivated(),\r\n onComplete: () => this.onImageDeactivated(),\r\n })\r\n .fromTo(\r\n img.DOM.el,\r\n {\r\n opacity: 1,\r\n filter: \"brightness(80%)\",\r\n scale: 0.1,\r\n zIndex: this.zIndexVal,\r\n x: this.cacheMousePos.x - (img.rect?.width ?? 0) / 2,\r\n y: this.cacheMousePos.y - (img.rect?.height ?? 0) / 2,\r\n rotation: startAngle,\r\n },\r\n {\r\n duration: 1,\r\n ease: \"power2\",\r\n scale: 1,\r\n filter: \"brightness(100%)\",\r\n x: this.mousePos.x - (img.rect?.width ?? 0) / 2 + dx * 70,\r\n y: this.mousePos.y - (img.rect?.height ?? 0) / 2 + dy * 70,\r\n rotation: this.lastAngle,\r\n },\r\n 0,\r\n )\r\n .to(\r\n img.DOM.el,\r\n {\r\n duration: 0.4,\r\n ease: \"expo\",\r\n opacity: 0,\r\n },\r\n 0.5,\r\n )\r\n .to(\r\n img.DOM.el,\r\n {\r\n duration: 1.5,\r\n ease: \"power4\",\r\n x: `+=${dx * 120}`,\r\n y: `+=${dy * 120}`,\r\n },\r\n 0.05,\r\n );\r\n }\r\n\r\n private onImageActivated() {\r\n this.activeImagesCount++;\r\n this.isIdle = false;\r\n }\r\n\r\n private onImageDeactivated() {\r\n this.activeImagesCount--;\r\n if (this.activeImagesCount === 0) this.isIdle = true;\r\n }\r\n}\r\n\r\nexport class ImageTrailVariant6 {\r\n private container: HTMLDivElement;\r\n private DOM: { el: HTMLDivElement };\r\n private images: ImageItem[];\r\n private imagesTotal: number;\r\n private imgPosition: number;\r\n private zIndexVal: number;\r\n private activeImagesCount: number;\r\n private isIdle: boolean;\r\n private threshold: number;\r\n private mousePos: { x: number; y: number };\r\n private lastMousePos: { x: number; y: number };\r\n private cacheMousePos: { x: number; y: number };\r\n\r\n constructor(container: HTMLDivElement) {\r\n this.container = container;\r\n this.DOM = { el: container };\r\n this.images = [...container.querySelectorAll(\".content__img\")].map(\r\n (img) => new ImageItem(img as HTMLDivElement),\r\n );\r\n this.imagesTotal = this.images.length;\r\n this.imgPosition = 0;\r\n this.zIndexVal = 1;\r\n this.activeImagesCount = 0;\r\n this.isIdle = true;\r\n this.threshold = 80;\r\n this.mousePos = { x: 0, y: 0 };\r\n this.lastMousePos = { x: 0, y: 0 };\r\n this.cacheMousePos = { x: 0, y: 0 };\r\n\r\n const handlePointerMove = (ev: MouseEvent | TouchEvent) => {\r\n const rect = container.getBoundingClientRect();\r\n this.mousePos = getLocalPointerPos(ev, rect);\r\n };\r\n container.addEventListener(\"mousemove\", handlePointerMove);\r\n container.addEventListener(\"touchmove\", handlePointerMove);\r\n\r\n const initRender = (ev: MouseEvent | TouchEvent) => {\r\n const rect = container.getBoundingClientRect();\r\n this.mousePos = getLocalPointerPos(ev, rect);\r\n this.cacheMousePos = { ...this.mousePos };\r\n requestAnimationFrame(() => this.render());\r\n container.removeEventListener(\"mousemove\", initRender as EventListener);\r\n container.removeEventListener(\"touchmove\", initRender as EventListener);\r\n };\r\n container.addEventListener(\"mousemove\", initRender as EventListener);\r\n container.addEventListener(\"touchmove\", initRender as EventListener);\r\n }\r\n\r\n private render() {\r\n const distance = getMouseDistance(this.mousePos, this.lastMousePos);\r\n this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.3);\r\n this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.3);\r\n\r\n if (distance > this.threshold) {\r\n this.showNextImage();\r\n this.lastMousePos = { ...this.mousePos };\r\n }\r\n if (this.isIdle && this.zIndexVal !== 1) {\r\n this.zIndexVal = 1;\r\n }\r\n requestAnimationFrame(() => this.render());\r\n }\r\n\r\n private mapSpeedToSize(speed: number, minSize: number, maxSize: number) {\r\n const maxSpeed = 200;\r\n return minSize + (maxSize - minSize) * Math.min(speed / maxSpeed, 1);\r\n }\r\n\r\n private mapSpeedToBrightness(speed: number, minB: number, maxB: number) {\r\n const maxSpeed = 70;\r\n return minB + (maxB - minB) * Math.min(speed / maxSpeed, 1);\r\n }\r\n\r\n private mapSpeedToBlur(speed: number, minBlur: number, maxBlur: number) {\r\n const maxSpeed = 90;\r\n return minBlur + (maxBlur - minBlur) * Math.min(speed / maxSpeed, 1);\r\n }\r\n\r\n private mapSpeedToGrayscale(speed: number, minG: number, maxG: number) {\r\n const maxSpeed = 90;\r\n return minG + (maxG - minG) * Math.min(speed / maxSpeed, 1);\r\n }\r\n\r\n private showNextImage() {\r\n const dx = this.mousePos.x - this.cacheMousePos.x;\r\n const dy = this.mousePos.y - this.cacheMousePos.y;\r\n const speed = Math.sqrt(dx * dx + dy * dy);\r\n\r\n ++this.zIndexVal;\r\n this.imgPosition = this.imgPosition < this.imagesTotal - 1 ? this.imgPosition + 1 : 0;\r\n const img = this.images[this.imgPosition];\r\n\r\n const scaleFactor = this.mapSpeedToSize(speed, 0.3, 2);\r\n const brightnessValue = this.mapSpeedToBrightness(speed, 0, 1.3);\r\n const blurValue = this.mapSpeedToBlur(speed, 20, 0);\r\n const grayscaleValue = this.mapSpeedToGrayscale(speed, 600, 0);\r\n\r\n gsap.killTweensOf(img.DOM.el);\r\n gsap\r\n .timeline({\r\n onStart: () => this.onImageActivated(),\r\n onComplete: () => this.onImageDeactivated(),\r\n })\r\n .fromTo(\r\n img.DOM.el,\r\n {\r\n opacity: 1,\r\n scale: 0,\r\n zIndex: this.zIndexVal,\r\n x: this.cacheMousePos.x - (img.rect?.width ?? 0) / 2,\r\n y: this.cacheMousePos.y - (img.rect?.height ?? 0) / 2,\r\n },\r\n {\r\n duration: 0.8,\r\n ease: \"power3\",\r\n scale: scaleFactor,\r\n filter: `grayscale(${grayscaleValue * 100}%) brightness(${\r\n brightnessValue * 100\r\n }%) blur(${blurValue}px)`,\r\n x: this.mousePos.x - (img.rect?.width ?? 0) / 2,\r\n y: this.mousePos.y - (img.rect?.height ?? 0) / 2,\r\n },\r\n 0,\r\n )\r\n .fromTo(\r\n img.DOM.inner,\r\n { scale: 2 },\r\n {\r\n duration: 0.8,\r\n ease: \"power3\",\r\n scale: 1,\r\n },\r\n 0,\r\n )\r\n .to(\r\n img.DOM.el,\r\n {\r\n duration: 0.4,\r\n ease: \"power3.in\",\r\n opacity: 0,\r\n scale: 0.2,\r\n },\r\n 0.45,\r\n );\r\n }\r\n\r\n private onImageActivated() {\r\n this.activeImagesCount++;\r\n this.isIdle = false;\r\n }\r\n\r\n private onImageDeactivated() {\r\n this.activeImagesCount--;\r\n if (this.activeImagesCount === 0) {\r\n this.isIdle = true;\r\n }\r\n }\r\n}\r\n\r\nexport class ImageTrailVariant7 {\r\n private container: HTMLDivElement;\r\n private DOM: { el: HTMLDivElement };\r\n private images: ImageItem[];\r\n private imagesTotal: number;\r\n private imgPosition: number;\r\n private zIndexVal: number;\r\n private activeImagesCount: number;\r\n private isIdle: boolean;\r\n private threshold: number;\r\n private mousePos: { x: number; y: number };\r\n private lastMousePos: { x: number; y: number };\r\n private cacheMousePos: { x: number; y: number };\r\n private visibleImagesCount: number;\r\n private visibleImagesTotal: number;\r\n\r\n constructor(container: HTMLDivElement) {\r\n this.container = container;\r\n this.DOM = { el: container };\r\n this.images = [...container.querySelectorAll(\".content__img\")].map(\r\n (img) => new ImageItem(img as HTMLDivElement),\r\n );\r\n this.imagesTotal = this.images.length;\r\n this.imgPosition = 0;\r\n this.zIndexVal = 1;\r\n this.activeImagesCount = 0;\r\n this.isIdle = true;\r\n this.threshold = 80;\r\n this.mousePos = { x: 0, y: 0 };\r\n this.lastMousePos = { x: 0, y: 0 };\r\n this.cacheMousePos = { x: 0, y: 0 };\r\n this.visibleImagesCount = 0;\r\n this.visibleImagesTotal = 9;\r\n this.visibleImagesTotal = Math.min(this.visibleImagesTotal, this.imagesTotal - 1);\r\n\r\n const handlePointerMove = (ev: MouseEvent | TouchEvent) => {\r\n const rect = container.getBoundingClientRect();\r\n this.mousePos = getLocalPointerPos(ev, rect);\r\n };\r\n container.addEventListener(\"mousemove\", handlePointerMove);\r\n container.addEventListener(\"touchmove\", handlePointerMove);\r\n\r\n const initRender = (ev: MouseEvent | TouchEvent) => {\r\n const rect = container.getBoundingClientRect();\r\n this.mousePos = getLocalPointerPos(ev, rect);\r\n this.cacheMousePos = { ...this.mousePos };\r\n requestAnimationFrame(() => this.render());\r\n container.removeEventListener(\"mousemove\", initRender as EventListener);\r\n container.removeEventListener(\"touchmove\", initRender as EventListener);\r\n };\r\n container.addEventListener(\"mousemove\", initRender as EventListener);\r\n container.addEventListener(\"touchmove\", initRender as EventListener);\r\n }\r\n\r\n private render() {\r\n const distance = getMouseDistance(this.mousePos, this.lastMousePos);\r\n this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.3);\r\n this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.3);\r\n\r\n if (distance > this.threshold) {\r\n this.showNextImage();\r\n this.lastMousePos = { ...this.mousePos };\r\n }\r\n if (this.isIdle && this.zIndexVal !== 1) this.zIndexVal = 1;\r\n\r\n requestAnimationFrame(() => this.render());\r\n }\r\n\r\n private showNextImage() {\r\n ++this.zIndexVal;\r\n this.imgPosition = this.imgPosition < this.imagesTotal - 1 ? this.imgPosition + 1 : 0;\r\n const img = this.images[this.imgPosition];\r\n ++this.visibleImagesCount;\r\n\r\n gsap.killTweensOf(img.DOM.el);\r\n const scaleValue = gsap.utils.random(0.5, 1.6);\r\n\r\n gsap\r\n .timeline({\r\n onStart: () => this.onImageActivated(),\r\n onComplete: () => this.onImageDeactivated(),\r\n })\r\n .fromTo(\r\n img.DOM.el,\r\n {\r\n scale: scaleValue - Math.max(gsap.utils.random(0.2, 0.6), 0),\r\n rotationZ: 0,\r\n opacity: 1,\r\n zIndex: this.zIndexVal,\r\n x: this.cacheMousePos.x - (img.rect?.width ?? 0) / 2,\r\n y: this.cacheMousePos.y - (img.rect?.height ?? 0) / 2,\r\n },\r\n {\r\n duration: 0.4,\r\n ease: \"power3\",\r\n scale: scaleValue,\r\n rotationZ: gsap.utils.random(-3, 3),\r\n x: this.mousePos.x - (img.rect?.width ?? 0) / 2,\r\n y: this.mousePos.y - (img.rect?.height ?? 0) / 2,\r\n },\r\n 0,\r\n );\r\n\r\n if (this.visibleImagesCount >= this.visibleImagesTotal) {\r\n const lastInQueue = getNewPosition(this.imgPosition, this.visibleImagesTotal, this.images);\r\n const oldImg = this.images[lastInQueue];\r\n gsap.to(oldImg.DOM.el, {\r\n duration: 0.4,\r\n ease: \"power4\",\r\n opacity: 0,\r\n scale: 1.3,\r\n onComplete: () => {\r\n if (this.activeImagesCount === 0) {\r\n this.isIdle = true;\r\n }\r\n },\r\n });\r\n }\r\n }\r\n\r\n private onImageActivated() {\r\n this.activeImagesCount++;\r\n this.isIdle = false;\r\n }\r\n\r\n private onImageDeactivated() {\r\n this.activeImagesCount--;\r\n }\r\n}\r\n\r\nexport class ImageTrailVariant8 {\r\n private container: HTMLDivElement;\r\n private DOM: { el: HTMLDivElement };\r\n private images: ImageItem[];\r\n private imagesTotal: number;\r\n private imgPosition: number;\r\n private zIndexVal: number;\r\n private activeImagesCount: number;\r\n private isIdle: boolean;\r\n private threshold: number;\r\n private mousePos: { x: number; y: number };\r\n private lastMousePos: { x: number; y: number };\r\n private cacheMousePos: { x: number; y: number };\r\n private rotation: { x: number; y: number };\r\n private cachedRotation: { x: number; y: number };\r\n private zValue: number;\r\n private cachedZValue: number;\r\n\r\n constructor(container: HTMLDivElement) {\r\n this.container = container;\r\n this.DOM = { el: container };\r\n this.images = [...container.querySelectorAll(\".content__img\")].map(\r\n (img) => new ImageItem(img as HTMLDivElement),\r\n );\r\n this.imagesTotal = this.images.length;\r\n this.imgPosition = 0;\r\n this.zIndexVal = 1;\r\n this.activeImagesCount = 0;\r\n this.isIdle = true;\r\n this.threshold = 80;\r\n this.mousePos = { x: 0, y: 0 };\r\n this.lastMousePos = { x: 0, y: 0 };\r\n this.cacheMousePos = { x: 0, y: 0 };\r\n this.rotation = { x: 0, y: 0 };\r\n this.cachedRotation = { x: 0, y: 0 };\r\n this.zValue = 0;\r\n this.cachedZValue = 0;\r\n\r\n const handlePointerMove = (ev: MouseEvent | TouchEvent) => {\r\n const rect = container.getBoundingClientRect();\r\n this.mousePos = getLocalPointerPos(ev, rect);\r\n };\r\n container.addEventListener(\"mousemove\", handlePointerMove);\r\n container.addEventListener(\"touchmove\", handlePointerMove);\r\n\r\n const initRender = (ev: MouseEvent | TouchEvent) => {\r\n const rect = container.getBoundingClientRect();\r\n this.mousePos = getLocalPointerPos(ev, rect);\r\n this.cacheMousePos = { ...this.mousePos };\r\n requestAnimationFrame(() => this.render());\r\n container.removeEventListener(\"mousemove\", initRender as EventListener);\r\n container.removeEventListener(\"touchmove\", initRender as EventListener);\r\n };\r\n container.addEventListener(\"mousemove\", initRender as EventListener);\r\n container.addEventListener(\"touchmove\", initRender as EventListener);\r\n }\r\n\r\n private render() {\r\n const distance = getMouseDistance(this.mousePos, this.lastMousePos);\r\n this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.1);\r\n this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.1);\r\n\r\n if (distance > this.threshold) {\r\n this.showNextImage();\r\n this.lastMousePos = { ...this.mousePos };\r\n }\r\n if (this.isIdle && this.zIndexVal !== 1) {\r\n this.zIndexVal = 1;\r\n }\r\n requestAnimationFrame(() => this.render());\r\n }\r\n\r\n private showNextImage() {\r\n const rect = this.container.getBoundingClientRect();\r\n const centerX = rect.width / 2;\r\n const centerY = rect.height / 2;\r\n const relX = this.mousePos.x - centerX;\r\n const relY = this.mousePos.y - centerY;\r\n\r\n this.rotation.x = -(relY / centerY) * 30;\r\n this.rotation.y = (relX / centerX) * 30;\r\n this.cachedRotation = { ...this.rotation };\r\n\r\n const distanceFromCenter = Math.sqrt(relX * relX + relY * relY);\r\n const maxDistance = Math.sqrt(centerX * centerX + centerY * centerY);\r\n const proportion = distanceFromCenter / maxDistance;\r\n this.zValue = proportion * 1200 - 600;\r\n this.cachedZValue = this.zValue;\r\n const normalizedZ = (this.zValue + 600) / 1200;\r\n const brightness = 0.2 + normalizedZ * 2.3;\r\n\r\n ++this.zIndexVal;\r\n this.imgPosition = this.imgPosition < this.imagesTotal - 1 ? this.imgPosition + 1 : 0;\r\n const img = this.images[this.imgPosition];\r\n gsap.killTweensOf(img.DOM.el);\r\n\r\n gsap\r\n .timeline({\r\n onStart: () => this.onImageActivated(),\r\n onComplete: () => this.onImageDeactivated(),\r\n })\r\n .set(this.DOM.el, { perspective: 1000 }, 0)\r\n .fromTo(\r\n img.DOM.el,\r\n {\r\n opacity: 1,\r\n z: 0,\r\n scale: 1 + this.cachedZValue / 1000,\r\n zIndex: this.zIndexVal,\r\n x: this.cacheMousePos.x - (img.rect?.width ?? 0) / 2,\r\n y: this.cacheMousePos.y - (img.rect?.height ?? 0) / 2,\r\n rotationX: this.cachedRotation.x,\r\n rotationY: this.cachedRotation.y,\r\n filter: `brightness(${brightness})`,\r\n },\r\n {\r\n duration: 1,\r\n ease: \"expo\",\r\n scale: 1 + this.zValue / 1000,\r\n x: this.mousePos.x - (img.rect?.width ?? 0) / 2,\r\n y: this.mousePos.y - (img.rect?.height ?? 0) / 2,\r\n rotationX: this.rotation.x,\r\n rotationY: this.rotation.y,\r\n },\r\n 0,\r\n )\r\n .to(\r\n img.DOM.el,\r\n {\r\n duration: 0.4,\r\n ease: \"power2\",\r\n opacity: 0,\r\n z: -800,\r\n },\r\n 0.3,\r\n );\r\n }\r\n\r\n private onImageActivated() {\r\n this.activeImagesCount++;\r\n this.isIdle = false;\r\n }\r\n\r\n private onImageDeactivated() {\r\n this.activeImagesCount--;\r\n if (this.activeImagesCount === 0) {\r\n this.isIdle = true;\r\n }\r\n }\r\n}\r\n\r\nfunction lerp(a: number, b: number, n: number): number {\r\n return (1 - n) * a + n * b;\r\n}\r\n\r\nfunction getLocalPointerPos(e: MouseEvent | TouchEvent, rect: DOMRect): { x: number; y: number } {\r\n let clientX = 0;\r\n let clientY = 0;\r\n if (\"touches\" in e && e.touches.length > 0) {\r\n clientX = e.touches[0].clientX;\r\n clientY = e.touches[0].clientY;\r\n } else if (\"clientX\" in e) {\r\n clientX = e.clientX;\r\n clientY = e.clientY;\r\n }\r\n return {\r\n x: clientX - rect.left,\r\n y: clientY - rect.top,\r\n };\r\n}\r\n\r\nfunction getMouseDistance(p1: { x: number; y: number }, p2: { x: number; y: number }): number {\r\n const dx = p1.x - p2.x;\r\n const dy = p1.y - p2.y;\r\n return Math.hypot(dx, dy);\r\n}\r\n\r\nfunction getNewPosition(position: number, offset: number, arr: ImageItem[]) {\r\n const realOffset = Math.abs(offset) % arr.length;\r\n if (position - realOffset >= 0) {\r\n return position - realOffset;\r\n } else {\r\n return arr.length - (realOffset - position);\r\n }\r\n}\r\n\r\nexport class ImageItem {\r\n public DOM: { el: HTMLDivElement; inner: HTMLDivElement | null } = {\r\n el: null as unknown as HTMLDivElement,\r\n inner: null,\r\n };\r\n public defaultStyle: gsap.TweenVars = { scale: 1, x: 0, y: 0, opacity: 0 };\r\n public rect: DOMRect | null = null;\r\n private resize!: () => void;\r\n\r\n constructor(DOM_el: HTMLDivElement) {\r\n this.DOM.el = DOM_el;\r\n this.DOM.inner = this.DOM.el.querySelector(\".content__img-inner\");\r\n this.getRect();\r\n this.initEvents();\r\n }\r\n\r\n private initEvents() {\r\n this.resize = () => {\r\n gsap.set(this.DOM.el, this.defaultStyle);\r\n this.getRect();\r\n };\r\n window.addEventListener(\"resize\", this.resize);\r\n }\r\n\r\n private getRect() {\r\n this.rect = this.DOM.el.getBoundingClientRect();\r\n }\r\n}\r\n\r\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\r\nexport const variantMap: { [key: string]: new (container: HTMLDivElement) => any } = {\r\n type1: ImageTrailVariant1,\r\n type2: ImageTrailVariant2,\r\n type3: ImageTrailVariant3,\r\n type4: ImageTrailVariant4,\r\n type5: ImageTrailVariant5,\r\n type6: ImageTrailVariant6,\r\n type7: ImageTrailVariant7,\r\n type8: ImageTrailVariant8,\r\n};\r\n"
18
+ }
19
+ ],
20
+ "fileCount": 3,
21
+ "contentHash": "4d91dd44e5982ddba5c2de0d35c03112694772ee"
22
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "images-slider",
3
+ "dependencies": [
4
+ "@vueuse/core"
5
+ ],
6
+ "files": [
7
+ {
8
+ "path": "ImagesSlider.vue",
9
+ "content": "<template>\r\n <div\r\n ref=\"sliderRef\"\r\n tabindex=\"0\"\r\n class=\"relative flex size-full items-center justify-center overflow-hidden transition-colors focus:outline-none focus:ring-1\"\r\n :style=\"{\r\n perspective: props.perspective,\r\n }\"\r\n >\r\n <Transition\r\n mode=\"out-in\"\r\n v-bind=\"transitionProps\"\r\n >\r\n <div\r\n :key=\"currentImage\"\r\n class=\"\"\r\n >\r\n <img\r\n :src=\"currentImage\"\r\n :class=\"props.imageClass\"\r\n />\r\n </div>\r\n </Transition>\r\n <div\r\n v-if=\"hideOverlay !== true\"\r\n :class=\"cn('absolute inset-0', props.overlayClass)\"\r\n >\r\n <Transition\r\n appear\r\n enter-active-class=\"transition-all duration-300 delay-300 ease-in-out\"\r\n enter-from-class=\"opacity-0 -translate-y-10\"\r\n >\r\n <slot\r\n v-if=\"!isLoading\"\r\n :current-index=\"currentIndex\"\r\n ></slot>\r\n </Transition>\r\n </div>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { useIntervalFn, onKeyStroke, useSwipe } from \"@vueuse/core\";\r\nimport { ref, watch, computed, type PropType, type Ref } from \"vue\";\r\nimport { cn } from \"@/lib/utils\";\r\n\r\nconst props = defineProps({\r\n images: {\r\n type: Array as PropType<string[]>,\r\n default: () => [],\r\n },\r\n hideOverlay: {\r\n type: Boolean,\r\n default: false,\r\n },\r\n overlayClass: {\r\n type: String,\r\n default: \"\",\r\n },\r\n imageClass: {\r\n type: String,\r\n default: \"bg-cover bg-center bg-no-repeat\",\r\n },\r\n enterFromClass: {\r\n type: String,\r\n default: \"scale-0 origin-center\",\r\n },\r\n enterActiveClass: {\r\n type: String,\r\n default: \"transition-transform duration-300 ease-in-out\",\r\n },\r\n leaveActiveClass: {\r\n type: String,\r\n default: \"transition-transform duration-300 ease-in-out\",\r\n },\r\n autoplay: {\r\n type: [Boolean, Number, String],\r\n default: false,\r\n },\r\n direction: {\r\n type: String,\r\n default: \"vertical\",\r\n validator: (v: string) => [\"vertical\", \"horizontal\"].includes(v),\r\n },\r\n perspective: {\r\n type: String,\r\n default: \"1000px\",\r\n },\r\n});\r\n\r\nconst sliderRef = ref(null);\r\nconst currentDirection = ref(\"up\");\r\nconst currentIndex = defineModel(\"modelValue\", {\r\n type: Number,\r\n default: 0,\r\n});\r\n\r\nfunction setCurrentDirection(dir: \"prev\" | \"next\") {\r\n if (props.direction === \"horizontal\") {\r\n currentDirection.value = dir === \"next\" ? \"left\" : \"right\";\r\n } else {\r\n currentDirection.value = dir === \"next\" ? \"up\" : \"down\";\r\n }\r\n}\r\n\r\n// Image Loading\r\nconst isLoading = ref(true);\r\nconst isTransitioning = ref(false);\r\nconst loadedImages: Ref<string[]> = ref([]);\r\nconst currentImage = computed(() => loadedImages.value[currentIndex.value]);\r\n\r\nfunction loadImages() {\r\n isLoading.value = true;\r\n const promises = props.images.map(\r\n (imageSrc): Promise<string> =>\r\n new Promise((resolve, reject) => {\r\n const image = new Image();\r\n image.src = imageSrc;\r\n image.onload = () => resolve(imageSrc);\r\n image.onerror = () => reject(imageSrc);\r\n }),\r\n );\r\n Promise.all(promises).then((resolvedImages) => {\r\n loadedImages.value = resolvedImages;\r\n isLoading.value = false;\r\n });\r\n}\r\nloadImages();\r\n\r\n// Navigation\r\n\r\nfunction onPrev() {\r\n if (isLoading.value || isTransitioning.value) {\r\n return false;\r\n }\r\n setCurrentDirection(\"prev\");\r\n let target = currentIndex.value - 1;\r\n if (target < 0) {\r\n target = loadedImages.value.length - 1;\r\n }\r\n currentIndex.value = target;\r\n}\r\n\r\nfunction onNext() {\r\n if (isLoading.value || isTransitioning.value) {\r\n return false;\r\n }\r\n setCurrentDirection(\"next\");\r\n let target = currentIndex.value + 1;\r\n if (target >= loadedImages.value?.length - 1) {\r\n target = 0;\r\n }\r\n currentIndex.value = target;\r\n}\r\n\r\nonKeyStroke(\r\n [\"ArrowUp\", \"ArrowDown\", \"ArrowLeft\", \"ArrowRight\"],\r\n (ev) => {\r\n ev.preventDefault();\r\n if (isLoading.value || isTransitioning.value) {\r\n return false;\r\n }\r\n pause();\r\n if ([\"ArrowUp\", \"ArrowLeft\"].includes(ev.key)) {\r\n onPrev();\r\n } else {\r\n onNext();\r\n }\r\n },\r\n {\r\n target: sliderRef,\r\n },\r\n);\r\n\r\nconst { direction: swipingDirection, isSwiping } = useSwipe(sliderRef, {\r\n passive: false,\r\n});\r\nwatch(swipingDirection, (dir) => {\r\n switch (dir) {\r\n case \"up\":\r\n case \"left\":\r\n return onPrev();\r\n case \"down\":\r\n case \"right\":\r\n return onNext();\r\n }\r\n});\r\n\r\nwatch(isSwiping, setSwiping);\r\n\r\nfunction setSwiping(v: boolean) {\r\n if (v) {\r\n pause();\r\n return;\r\n }\r\n\r\n resume();\r\n}\r\n\r\n// Autoplay\r\nconst autoplayInterval = computed(() => {\r\n if (props.autoplay === false) return 0;\r\n else if (props.autoplay === true) return 5000;\r\n else if (typeof props.autoplay === \"string\") {\r\n return +props.autoplay;\r\n } else return props.autoplay;\r\n});\r\n\r\nconst { pause, resume, isActive } = useIntervalFn(() => {\r\n onNext();\r\n}, autoplayInterval);\r\n\r\nwatch(isLoading, (v) => {\r\n if (v) pause();\r\n else resume();\r\n});\r\n\r\n// Transitions\r\nfunction lockViewport() {\r\n isTransitioning.value = true;\r\n}\r\n\r\nfunction unlockViewport() {\r\n isTransitioning.value = false;\r\n if (isActive.value === false && autoplayInterval.value) {\r\n resume();\r\n }\r\n}\r\nconst transitionProps = computed(() => {\r\n const bind = {\r\n enterActiveClass: props.enterActiveClass,\r\n leaveActiveClass: props.leaveActiveClass,\r\n enterFromClass: props.enterFromClass,\r\n leaveToClass: \"\",\r\n onBeforeLeave: lockViewport,\r\n onAfterEnter: unlockViewport,\r\n };\r\n\r\n switch (currentDirection.value) {\r\n case \"up\":\r\n bind.leaveToClass = \"-translate-y-full\";\r\n break;\r\n case \"down\":\r\n bind.leaveToClass = \"translate-y-full\";\r\n break;\r\n case \"left\":\r\n bind.leaveToClass = \"-translate-x-full\";\r\n break;\r\n case \"right\":\r\n bind.leaveToClass = \"translate-x-full\";\r\n break;\r\n }\r\n\r\n return bind;\r\n});\r\n</script>\r\n"
10
+ },
11
+ {
12
+ "path": "index.ts",
13
+ "content": "export { default as ImagesSlider } from \"./ImagesSlider.vue\";\r\n"
14
+ }
15
+ ],
16
+ "fileCount": 2,
17
+ "contentHash": "2a6ead55b26628bfab5356a52b226c18b250984c"
18
+ }