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

1 line
39 KiB
JSON

{"name":"ImageTrail","title":"ImageTrail","description":"Cursor-based image trail with several built-in variants.","type":"registry:component","add":"when-added","files":[{"type":"registry:component","role":"file","content":"<script setup lang=\"ts\">\nimport { nextTick, onMounted, useTemplateRef } from 'vue';\nimport { gsap } from 'gsap';\n\nfunction lerp(a: number, b: number, n: number): number {\n return (1 - n) * a + n * b;\n}\n\nfunction getLocalPointerPos(e: MouseEvent | TouchEvent, rect: DOMRect): { x: number; y: number } {\n let clientX = 0,\n clientY = 0;\n if ('touches' in e && e.touches.length > 0) {\n clientX = e.touches[0].clientX;\n clientY = e.touches[0].clientY;\n } else if ('clientX' in e) {\n clientX = e.clientX;\n clientY = e.clientY;\n }\n return {\n x: clientX - rect.left,\n y: clientY - rect.top\n };\n}\n\nfunction getMouseDistance(p1: { x: number; y: number }, p2: { x: number; y: number }): number {\n const dx = p1.x - p2.x;\n const dy = p1.y - p2.y;\n return Math.hypot(dx, dy);\n}\n\nclass ImageItem {\n public DOM: { el: HTMLDivElement; inner: HTMLDivElement | null } = {\n el: null as unknown as HTMLDivElement,\n inner: null\n };\n public defaultStyle: gsap.TweenVars = { scale: 1, x: 0, y: 0, opacity: 0 };\n public rect: DOMRect | null = null;\n private resize!: () => void;\n\n constructor(DOM_el: HTMLDivElement) {\n this.DOM.el = DOM_el;\n this.DOM.inner = this.DOM.el.querySelector('.content__img-inner');\n this.getRect();\n this.initEvents();\n }\n\n private initEvents() {\n this.resize = () => {\n gsap.set(this.DOM.el, this.defaultStyle);\n this.getRect();\n };\n window.addEventListener('resize', this.resize);\n }\n\n private getRect() {\n this.rect = this.DOM.el.getBoundingClientRect();\n }\n}\n\nclass ImageTrailVariant1 {\n private container: HTMLDivElement;\n private DOM: { el: HTMLDivElement };\n private images: ImageItem[];\n private imagesTotal: number;\n private imgPosition: number;\n private zIndexVal: number;\n private activeImagesCount: number;\n private isIdle: boolean;\n private threshold: number;\n private mousePos: { x: number; y: number };\n private lastMousePos: { x: number; y: number };\n private cacheMousePos: { x: number; y: number };\n\n constructor(container: HTMLDivElement) {\n this.container = container;\n this.DOM = { el: container };\n this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img as HTMLDivElement));\n this.imagesTotal = this.images.length;\n this.imgPosition = 0;\n this.zIndexVal = 1;\n this.activeImagesCount = 0;\n this.isIdle = true;\n this.threshold = 80;\n this.mousePos = { x: 0, y: 0 };\n this.lastMousePos = { x: 0, y: 0 };\n this.cacheMousePos = { x: 0, y: 0 };\n\n const handlePointerMove = (ev: MouseEvent | TouchEvent) => {\n const rect = this.container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n };\n container.addEventListener('mousemove', handlePointerMove);\n container.addEventListener('touchmove', handlePointerMove);\n\n const initRender = (ev: MouseEvent | TouchEvent) => {\n const rect = this.container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n this.cacheMousePos = { ...this.mousePos };\n requestAnimationFrame(() => this.render());\n container.removeEventListener('mousemove', initRender as EventListener);\n container.removeEventListener('touchmove', initRender as EventListener);\n };\n container.addEventListener('mousemove', initRender as EventListener);\n container.addEventListener('touchmove', initRender as EventListener);\n }\n\n private render() {\n const distance = getMouseDistance(this.mousePos, this.lastMousePos);\n this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.1);\n this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.1);\n\n if (distance > this.threshold) {\n this.showNextImage();\n this.lastMousePos = { ...this.mousePos };\n }\n if (this.isIdle && this.zIndexVal !== 1) {\n this.zIndexVal = 1;\n }\n requestAnimationFrame(() => this.render());\n }\n\n private showNextImage() {\n ++this.zIndexVal;\n this.imgPosition = this.imgPosition < this.imagesTotal - 1 ? this.imgPosition + 1 : 0;\n const img = this.images[this.imgPosition];\n\n gsap.killTweensOf(img.DOM.el);\n gsap\n .timeline({\n onStart: () => this.onImageActivated(),\n onComplete: () => this.onImageDeactivated()\n })\n .fromTo(\n img.DOM.el,\n {\n opacity: 1,\n scale: 1,\n zIndex: this.zIndexVal,\n x: this.cacheMousePos.x - (img.rect?.width ?? 0) / 2,\n y: this.cacheMousePos.y - (img.rect?.height ?? 0) / 2\n },\n {\n duration: 0.4,\n ease: 'power1',\n x: this.mousePos.x - (img.rect?.width ?? 0) / 2,\n y: this.mousePos.y - (img.rect?.height ?? 0) / 2\n },\n 0\n )\n .to(\n img.DOM.el,\n {\n duration: 0.4,\n ease: 'power3',\n opacity: 0,\n scale: 0.2\n },\n 0.4\n );\n }\n\n private onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n\n private onImageDeactivated() {\n this.activeImagesCount--;\n if (this.activeImagesCount === 0) {\n this.isIdle = true;\n }\n }\n}\n\nclass ImageTrailVariant2 {\n private container: HTMLDivElement;\n private DOM: { el: HTMLDivElement };\n private images: ImageItem[];\n private imagesTotal: number;\n private imgPosition: number;\n private zIndexVal: number;\n private activeImagesCount: number;\n private isIdle: boolean;\n private threshold: number;\n private mousePos: { x: number; y: number };\n private lastMousePos: { x: number; y: number };\n private cacheMousePos: { x: number; y: number };\n\n constructor(container: HTMLDivElement) {\n this.container = container;\n this.DOM = { el: container };\n this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img as HTMLDivElement));\n this.imagesTotal = this.images.length;\n this.imgPosition = 0;\n this.zIndexVal = 1;\n this.activeImagesCount = 0;\n this.isIdle = true;\n this.threshold = 80;\n this.mousePos = { x: 0, y: 0 };\n this.lastMousePos = { x: 0, y: 0 };\n this.cacheMousePos = { x: 0, y: 0 };\n\n const handlePointerMove = (ev: MouseEvent | TouchEvent) => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n };\n container.addEventListener('mousemove', handlePointerMove);\n container.addEventListener('touchmove', handlePointerMove);\n\n const initRender = (ev: MouseEvent | TouchEvent) => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n this.cacheMousePos = { ...this.mousePos };\n requestAnimationFrame(() => this.render());\n container.removeEventListener('mousemove', initRender as EventListener);\n container.removeEventListener('touchmove', initRender as EventListener);\n };\n container.addEventListener('mousemove', initRender as EventListener);\n container.addEventListener('touchmove', initRender as EventListener);\n }\n\n private render() {\n const distance = getMouseDistance(this.mousePos, this.lastMousePos);\n this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.1);\n this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.1);\n\n if (distance > this.threshold) {\n this.showNextImage();\n this.lastMousePos = { ...this.mousePos };\n }\n if (this.isIdle && this.zIndexVal !== 1) {\n this.zIndexVal = 1;\n }\n requestAnimationFrame(() => this.render());\n }\n\n private showNextImage() {\n ++this.zIndexVal;\n this.imgPosition = this.imgPosition < this.imagesTotal - 1 ? this.imgPosition + 1 : 0;\n const img = this.images[this.imgPosition];\n\n gsap.killTweensOf(img.DOM.el);\n gsap\n .timeline({\n onStart: () => this.onImageActivated(),\n onComplete: () => this.onImageDeactivated()\n })\n .fromTo(\n img.DOM.el,\n {\n opacity: 1,\n scale: 0,\n zIndex: this.zIndexVal,\n x: this.cacheMousePos.x - (img.rect?.width ?? 0) / 2,\n y: this.cacheMousePos.y - (img.rect?.height ?? 0) / 2\n },\n {\n duration: 0.4,\n ease: 'power1',\n scale: 1,\n x: this.mousePos.x - (img.rect?.width ?? 0) / 2,\n y: this.mousePos.y - (img.rect?.height ?? 0) / 2\n },\n 0\n )\n .fromTo(\n img.DOM.inner,\n { scale: 2.8, filter: 'brightness(250%)' },\n {\n duration: 0.4,\n ease: 'power1',\n scale: 1,\n filter: 'brightness(100%)'\n },\n 0\n )\n .to(\n img.DOM.el,\n {\n duration: 0.4,\n ease: 'power2',\n opacity: 0,\n scale: 0.2\n },\n 0.45\n );\n }\n\n private onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n\n private onImageDeactivated() {\n this.activeImagesCount--;\n if (this.activeImagesCount === 0) {\n this.isIdle = true;\n }\n }\n}\n\nclass ImageTrailVariant3 {\n private container: HTMLDivElement;\n private DOM: { el: HTMLDivElement };\n private images: ImageItem[];\n private imagesTotal: number;\n private imgPosition: number;\n private zIndexVal: number;\n private activeImagesCount: number;\n private isIdle: boolean;\n private threshold: number;\n private mousePos: { x: number; y: number };\n private lastMousePos: { x: number; y: number };\n private cacheMousePos: { x: number; y: number };\n\n constructor(container: HTMLDivElement) {\n this.container = container;\n this.DOM = { el: container };\n this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img as HTMLDivElement));\n this.imagesTotal = this.images.length;\n this.imgPosition = 0;\n this.zIndexVal = 1;\n this.activeImagesCount = 0;\n this.isIdle = true;\n this.threshold = 80;\n this.mousePos = { x: 0, y: 0 };\n this.lastMousePos = { x: 0, y: 0 };\n this.cacheMousePos = { x: 0, y: 0 };\n\n const handlePointerMove = (ev: MouseEvent | TouchEvent) => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n };\n container.addEventListener('mousemove', handlePointerMove);\n container.addEventListener('touchmove', handlePointerMove);\n\n const initRender = (ev: MouseEvent | TouchEvent) => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n this.cacheMousePos = { ...this.mousePos };\n requestAnimationFrame(() => this.render());\n container.removeEventListener('mousemove', initRender as EventListener);\n container.removeEventListener('touchmove', initRender as EventListener);\n };\n container.addEventListener('mousemove', initRender as EventListener);\n container.addEventListener('touchmove', initRender as EventListener);\n }\n\n private render() {\n const distance = getMouseDistance(this.mousePos, this.lastMousePos);\n this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.1);\n this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.1);\n\n if (distance > this.threshold) {\n this.showNextImage();\n this.lastMousePos = { ...this.mousePos };\n }\n if (this.isIdle && this.zIndexVal !== 1) {\n this.zIndexVal = 1;\n }\n requestAnimationFrame(() => this.render());\n }\n\n private showNextImage() {\n ++this.zIndexVal;\n this.imgPosition = this.imgPosition < this.imagesTotal - 1 ? this.imgPosition + 1 : 0;\n const img = this.images[this.imgPosition];\n\n gsap.killTweensOf(img.DOM.el);\n gsap\n .timeline({\n onStart: () => this.onImageActivated(),\n onComplete: () => this.onImageDeactivated()\n })\n .fromTo(\n img.DOM.el,\n {\n opacity: 1,\n scale: 0,\n zIndex: this.zIndexVal,\n xPercent: 0,\n yPercent: 0,\n x: this.cacheMousePos.x - (img.rect?.width ?? 0) / 2,\n y: this.cacheMousePos.y - (img.rect?.height ?? 0) / 2\n },\n {\n duration: 0.4,\n ease: 'power1',\n scale: 1,\n x: this.mousePos.x - (img.rect?.width ?? 0) / 2,\n y: this.mousePos.y - (img.rect?.height ?? 0) / 2\n },\n 0\n )\n .fromTo(\n img.DOM.inner,\n { scale: 1.2 },\n {\n duration: 0.4,\n ease: 'power1',\n scale: 1\n },\n 0\n )\n .to(\n img.DOM.el,\n {\n duration: 0.6,\n ease: 'power2',\n opacity: 0,\n scale: 0.2,\n xPercent: () => gsap.utils.random(-30, 30),\n yPercent: -200\n },\n 0.6\n );\n }\n\n private onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n\n private onImageDeactivated() {\n this.activeImagesCount--;\n if (this.activeImagesCount === 0) {\n this.isIdle = true;\n }\n }\n}\n\nclass ImageTrailVariant4 {\n private container: HTMLDivElement;\n private DOM: { el: HTMLDivElement };\n private images: ImageItem[];\n private imagesTotal: number;\n private imgPosition: number;\n private zIndexVal: number;\n private activeImagesCount: number;\n private isIdle: boolean;\n private threshold: number;\n private mousePos: { x: number; y: number };\n private lastMousePos: { x: number; y: number };\n private cacheMousePos: { x: number; y: number };\n\n constructor(container: HTMLDivElement) {\n this.container = container;\n this.DOM = { el: container };\n this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img as HTMLDivElement));\n this.imagesTotal = this.images.length;\n this.imgPosition = 0;\n this.zIndexVal = 1;\n this.activeImagesCount = 0;\n this.isIdle = true;\n this.threshold = 80;\n this.mousePos = { x: 0, y: 0 };\n this.lastMousePos = { x: 0, y: 0 };\n this.cacheMousePos = { x: 0, y: 0 };\n\n const handlePointerMove = (ev: MouseEvent | TouchEvent) => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n };\n container.addEventListener('mousemove', handlePointerMove);\n container.addEventListener('touchmove', handlePointerMove);\n\n const initRender = (ev: MouseEvent | TouchEvent) => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n this.cacheMousePos = { ...this.mousePos };\n requestAnimationFrame(() => this.render());\n container.removeEventListener('mousemove', initRender as EventListener);\n container.removeEventListener('touchmove', initRender as EventListener);\n };\n container.addEventListener('mousemove', initRender as EventListener);\n container.addEventListener('touchmove', initRender as EventListener);\n }\n\n private render() {\n const distance = getMouseDistance(this.mousePos, this.lastMousePos);\n if (distance > this.threshold) {\n this.showNextImage();\n this.lastMousePos = { ...this.mousePos };\n }\n this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.1);\n this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.1);\n\n if (this.isIdle && this.zIndexVal !== 1) this.zIndexVal = 1;\n requestAnimationFrame(() => this.render());\n }\n\n private showNextImage() {\n ++this.zIndexVal;\n this.imgPosition = this.imgPosition < this.imagesTotal - 1 ? this.imgPosition + 1 : 0;\n const img = this.images[this.imgPosition];\n gsap.killTweensOf(img.DOM.el);\n\n let dx = this.mousePos.x - this.cacheMousePos.x;\n let dy = this.mousePos.y - this.cacheMousePos.y;\n const distance = Math.sqrt(dx * dx + dy * dy);\n if (distance !== 0) {\n dx /= distance;\n dy /= distance;\n }\n dx *= distance / 100;\n dy *= distance / 100;\n\n gsap\n .timeline({\n onStart: () => this.onImageActivated(),\n onComplete: () => this.onImageDeactivated()\n })\n .fromTo(\n img.DOM.el,\n {\n opacity: 1,\n scale: 0,\n zIndex: this.zIndexVal,\n x: this.cacheMousePos.x - (img.rect?.width ?? 0) / 2,\n y: this.cacheMousePos.y - (img.rect?.height ?? 0) / 2\n },\n {\n duration: 0.4,\n ease: 'power1',\n scale: 1,\n x: this.mousePos.x - (img.rect?.width ?? 0) / 2,\n y: this.mousePos.y - (img.rect?.height ?? 0) / 2\n },\n 0\n )\n .fromTo(\n img.DOM.inner,\n {\n scale: 2,\n filter: `brightness(${Math.max((400 * distance) / 100, 100)}%) contrast(${Math.max(\n (400 * distance) / 100,\n 100\n )}%)`\n },\n {\n duration: 0.4,\n ease: 'power1',\n scale: 1,\n filter: 'brightness(100%) contrast(100%)'\n },\n 0\n )\n .to(\n img.DOM.el,\n {\n duration: 0.4,\n ease: 'power3',\n opacity: 0\n },\n 0.4\n )\n .to(\n img.DOM.el,\n {\n duration: 1.5,\n ease: 'power4',\n x: `+=${dx * 110}`,\n y: `+=${dy * 110}`\n },\n 0.05\n );\n }\n\n private onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n\n private onImageDeactivated() {\n this.activeImagesCount--;\n if (this.activeImagesCount === 0) {\n this.isIdle = true;\n }\n }\n}\n\nclass ImageTrailVariant5 {\n private container: HTMLDivElement;\n private DOM: { el: HTMLDivElement };\n private images: ImageItem[];\n private imagesTotal: number;\n private imgPosition: number;\n private zIndexVal: number;\n private activeImagesCount: number;\n private isIdle: boolean;\n private threshold: number;\n private mousePos: { x: number; y: number };\n private lastMousePos: { x: number; y: number };\n private cacheMousePos: { x: number; y: number };\n private lastAngle: number;\n\n constructor(container: HTMLDivElement) {\n this.container = container;\n this.DOM = { el: container };\n this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img as HTMLDivElement));\n this.imagesTotal = this.images.length;\n this.imgPosition = 0;\n this.zIndexVal = 1;\n this.activeImagesCount = 0;\n this.isIdle = true;\n this.threshold = 80;\n this.mousePos = { x: 0, y: 0 };\n this.lastMousePos = { x: 0, y: 0 };\n this.cacheMousePos = { x: 0, y: 0 };\n this.lastAngle = 0;\n\n const handlePointerMove = (ev: MouseEvent | TouchEvent) => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n };\n container.addEventListener('mousemove', handlePointerMove);\n container.addEventListener('touchmove', handlePointerMove);\n\n const initRender = (ev: MouseEvent | TouchEvent) => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n this.cacheMousePos = { ...this.mousePos };\n requestAnimationFrame(() => this.render());\n container.removeEventListener('mousemove', initRender as EventListener);\n container.removeEventListener('touchmove', initRender as EventListener);\n };\n container.addEventListener('mousemove', initRender as EventListener);\n container.addEventListener('touchmove', initRender as EventListener);\n }\n\n private render() {\n const distance = getMouseDistance(this.mousePos, this.lastMousePos);\n if (distance > this.threshold) {\n this.showNextImage();\n this.lastMousePos = { ...this.mousePos };\n }\n this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.1);\n this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.1);\n if (this.isIdle && this.zIndexVal !== 1) this.zIndexVal = 1;\n requestAnimationFrame(() => this.render());\n }\n\n private showNextImage() {\n let dx = this.mousePos.x - this.cacheMousePos.x;\n let dy = this.mousePos.y - this.cacheMousePos.y;\n let angle = Math.atan2(dy, dx) * (180 / Math.PI);\n if (angle < 0) angle += 360;\n if (angle > 90 && angle <= 270) angle += 180;\n const isMovingClockwise = angle >= this.lastAngle;\n this.lastAngle = angle;\n const startAngle = isMovingClockwise ? angle - 10 : angle + 10;\n const distance = Math.sqrt(dx * dx + dy * dy);\n if (distance !== 0) {\n dx /= distance;\n dy /= distance;\n }\n dx *= distance / 150;\n dy *= distance / 150;\n\n ++this.zIndexVal;\n this.imgPosition = this.imgPosition < this.imagesTotal - 1 ? this.imgPosition + 1 : 0;\n const img = this.images[this.imgPosition];\n gsap.killTweensOf(img.DOM.el);\n\n gsap\n .timeline({\n onStart: () => this.onImageActivated(),\n onComplete: () => this.onImageDeactivated()\n })\n .fromTo(\n img.DOM.el,\n {\n opacity: 1,\n filter: 'brightness(80%)',\n scale: 0.1,\n zIndex: this.zIndexVal,\n x: this.cacheMousePos.x - (img.rect?.width ?? 0) / 2,\n y: this.cacheMousePos.y - (img.rect?.height ?? 0) / 2,\n rotation: startAngle\n },\n {\n duration: 1,\n ease: 'power2',\n scale: 1,\n filter: 'brightness(100%)',\n x: this.mousePos.x - (img.rect?.width ?? 0) / 2 + dx * 70,\n y: this.mousePos.y - (img.rect?.height ?? 0) / 2 + dy * 70,\n rotation: this.lastAngle\n },\n 0\n )\n .to(\n img.DOM.el,\n {\n duration: 0.4,\n ease: 'expo',\n opacity: 0\n },\n 0.5\n )\n .to(\n img.DOM.el,\n {\n duration: 1.5,\n ease: 'power4',\n x: `+=${dx * 120}`,\n y: `+=${dy * 120}`\n },\n 0.05\n );\n }\n\n private onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n\n private onImageDeactivated() {\n this.activeImagesCount--;\n if (this.activeImagesCount === 0) this.isIdle = true;\n }\n}\n\nclass ImageTrailVariant6 {\n private container: HTMLDivElement;\n private DOM: { el: HTMLDivElement };\n private images: ImageItem[];\n private imagesTotal: number;\n private imgPosition: number;\n private zIndexVal: number;\n private activeImagesCount: number;\n private isIdle: boolean;\n private threshold: number;\n private mousePos: { x: number; y: number };\n private lastMousePos: { x: number; y: number };\n private cacheMousePos: { x: number; y: number };\n\n constructor(container: HTMLDivElement) {\n this.container = container;\n this.DOM = { el: container };\n this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img as HTMLDivElement));\n this.imagesTotal = this.images.length;\n this.imgPosition = 0;\n this.zIndexVal = 1;\n this.activeImagesCount = 0;\n this.isIdle = true;\n this.threshold = 80;\n this.mousePos = { x: 0, y: 0 };\n this.lastMousePos = { x: 0, y: 0 };\n this.cacheMousePos = { x: 0, y: 0 };\n\n const handlePointerMove = (ev: MouseEvent | TouchEvent) => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n };\n container.addEventListener('mousemove', handlePointerMove);\n container.addEventListener('touchmove', handlePointerMove);\n\n const initRender = (ev: MouseEvent | TouchEvent) => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n this.cacheMousePos = { ...this.mousePos };\n requestAnimationFrame(() => this.render());\n container.removeEventListener('mousemove', initRender as EventListener);\n container.removeEventListener('touchmove', initRender as EventListener);\n };\n container.addEventListener('mousemove', initRender as EventListener);\n container.addEventListener('touchmove', initRender as EventListener);\n }\n\n private render() {\n const distance = getMouseDistance(this.mousePos, this.lastMousePos);\n this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.3);\n this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.3);\n\n if (distance > this.threshold) {\n this.showNextImage();\n this.lastMousePos = { ...this.mousePos };\n }\n if (this.isIdle && this.zIndexVal !== 1) {\n this.zIndexVal = 1;\n }\n requestAnimationFrame(() => this.render());\n }\n\n private mapSpeedToSize(speed: number, minSize: number, maxSize: number) {\n const maxSpeed = 200;\n return minSize + (maxSize - minSize) * Math.min(speed / maxSpeed, 1);\n }\n\n private mapSpeedToBrightness(speed: number, minB: number, maxB: number) {\n const maxSpeed = 70;\n return minB + (maxB - minB) * Math.min(speed / maxSpeed, 1);\n }\n\n private mapSpeedToBlur(speed: number, minBlur: number, maxBlur: number) {\n const maxSpeed = 90;\n return minBlur + (maxBlur - minBlur) * Math.min(speed / maxSpeed, 1);\n }\n\n private mapSpeedToGrayscale(speed: number, minG: number, maxG: number) {\n const maxSpeed = 90;\n return minG + (maxG - minG) * Math.min(speed / maxSpeed, 1);\n }\n\n private showNextImage() {\n const dx = this.mousePos.x - this.cacheMousePos.x;\n const dy = this.mousePos.y - this.cacheMousePos.y;\n const speed = Math.sqrt(dx * dx + dy * dy);\n\n ++this.zIndexVal;\n this.imgPosition = this.imgPosition < this.imagesTotal - 1 ? this.imgPosition + 1 : 0;\n const img = this.images[this.imgPosition];\n\n const scaleFactor = this.mapSpeedToSize(speed, 0.3, 2);\n const brightnessValue = this.mapSpeedToBrightness(speed, 0, 1.3);\n const blurValue = this.mapSpeedToBlur(speed, 20, 0);\n const grayscaleValue = this.mapSpeedToGrayscale(speed, 600, 0);\n\n gsap.killTweensOf(img.DOM.el);\n gsap\n .timeline({\n onStart: () => this.onImageActivated(),\n onComplete: () => this.onImageDeactivated()\n })\n .fromTo(\n img.DOM.el,\n {\n opacity: 1,\n scale: 0,\n zIndex: this.zIndexVal,\n x: this.cacheMousePos.x - (img.rect?.width ?? 0) / 2,\n y: this.cacheMousePos.y - (img.rect?.height ?? 0) / 2\n },\n {\n duration: 0.8,\n ease: 'power3',\n scale: scaleFactor,\n filter: `grayscale(${grayscaleValue * 100}%) brightness(${brightnessValue * 100}%) blur(${blurValue}px)`,\n x: this.mousePos.x - (img.rect?.width ?? 0) / 2,\n y: this.mousePos.y - (img.rect?.height ?? 0) / 2\n },\n 0\n )\n .fromTo(\n img.DOM.inner,\n { scale: 2 },\n {\n duration: 0.8,\n ease: 'power3',\n scale: 1\n },\n 0\n )\n .to(\n img.DOM.el,\n {\n duration: 0.4,\n ease: 'power3.in',\n opacity: 0,\n scale: 0.2\n },\n 0.45\n );\n }\n\n private onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n\n private onImageDeactivated() {\n this.activeImagesCount--;\n if (this.activeImagesCount === 0) {\n this.isIdle = true;\n }\n }\n}\n\nfunction getNewPosition(position: number, offset: number, arr: ImageItem[]) {\n const realOffset = Math.abs(offset) % arr.length;\n if (position - realOffset >= 0) {\n return position - realOffset;\n } else {\n return arr.length - (realOffset - position);\n }\n}\n\nclass ImageTrailVariant7 {\n private container: HTMLDivElement;\n private DOM: { el: HTMLDivElement };\n private images: ImageItem[];\n private imagesTotal: number;\n private imgPosition: number;\n private zIndexVal: number;\n private activeImagesCount: number;\n private isIdle: boolean;\n private threshold: number;\n private mousePos: { x: number; y: number };\n private lastMousePos: { x: number; y: number };\n private cacheMousePos: { x: number; y: number };\n private visibleImagesCount: number;\n private visibleImagesTotal: number;\n\n constructor(container: HTMLDivElement) {\n this.container = container;\n this.DOM = { el: container };\n this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img as HTMLDivElement));\n this.imagesTotal = this.images.length;\n this.imgPosition = 0;\n this.zIndexVal = 1;\n this.activeImagesCount = 0;\n this.isIdle = true;\n this.threshold = 80;\n this.mousePos = { x: 0, y: 0 };\n this.lastMousePos = { x: 0, y: 0 };\n this.cacheMousePos = { x: 0, y: 0 };\n this.visibleImagesCount = 0;\n this.visibleImagesTotal = 9;\n this.visibleImagesTotal = Math.min(this.visibleImagesTotal, this.imagesTotal - 1);\n\n const handlePointerMove = (ev: MouseEvent | TouchEvent) => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n };\n container.addEventListener('mousemove', handlePointerMove);\n container.addEventListener('touchmove', handlePointerMove);\n\n const initRender = (ev: MouseEvent | TouchEvent) => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n this.cacheMousePos = { ...this.mousePos };\n requestAnimationFrame(() => this.render());\n container.removeEventListener('mousemove', initRender as EventListener);\n container.removeEventListener('touchmove', initRender as EventListener);\n };\n container.addEventListener('mousemove', initRender as EventListener);\n container.addEventListener('touchmove', initRender as EventListener);\n }\n\n private render() {\n const distance = getMouseDistance(this.mousePos, this.lastMousePos);\n this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.3);\n this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.3);\n\n if (distance > this.threshold) {\n this.showNextImage();\n this.lastMousePos = { ...this.mousePos };\n }\n if (this.isIdle && this.zIndexVal !== 1) this.zIndexVal = 1;\n\n requestAnimationFrame(() => this.render());\n }\n\n private showNextImage() {\n ++this.zIndexVal;\n this.imgPosition = this.imgPosition < this.imagesTotal - 1 ? this.imgPosition + 1 : 0;\n const img = this.images[this.imgPosition];\n ++this.visibleImagesCount;\n\n gsap.killTweensOf(img.DOM.el);\n const scaleValue = gsap.utils.random(0.5, 1.6);\n\n gsap\n .timeline({\n onStart: () => this.onImageActivated(),\n onComplete: () => this.onImageDeactivated()\n })\n .fromTo(\n img.DOM.el,\n {\n scale: scaleValue - Math.max(gsap.utils.random(0.2, 0.6), 0),\n rotationZ: 0,\n opacity: 1,\n zIndex: this.zIndexVal,\n x: this.cacheMousePos.x - (img.rect?.width ?? 0) / 2,\n y: this.cacheMousePos.y - (img.rect?.height ?? 0) / 2\n },\n {\n duration: 0.4,\n ease: 'power3',\n scale: scaleValue,\n rotationZ: gsap.utils.random(-3, 3),\n x: this.mousePos.x - (img.rect?.width ?? 0) / 2,\n y: this.mousePos.y - (img.rect?.height ?? 0) / 2\n },\n 0\n );\n\n if (this.visibleImagesCount >= this.visibleImagesTotal) {\n const lastInQueue = getNewPosition(this.imgPosition, this.visibleImagesTotal, this.images);\n const oldImg = this.images[lastInQueue];\n gsap.to(oldImg.DOM.el, {\n duration: 0.4,\n ease: 'power4',\n opacity: 0,\n scale: 1.3,\n onComplete: () => {\n if (this.activeImagesCount === 0) {\n this.isIdle = true;\n }\n }\n });\n }\n }\n\n private onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n\n private onImageDeactivated() {\n this.activeImagesCount--;\n }\n}\n\nclass ImageTrailVariant8 {\n private container: HTMLDivElement;\n private DOM: { el: HTMLDivElement };\n private images: ImageItem[];\n private imagesTotal: number;\n private imgPosition: number;\n private zIndexVal: number;\n private activeImagesCount: number;\n private isIdle: boolean;\n private threshold: number;\n private mousePos: { x: number; y: number };\n private lastMousePos: { x: number; y: number };\n private cacheMousePos: { x: number; y: number };\n private rotation: { x: number; y: number };\n private cachedRotation: { x: number; y: number };\n private zValue: number;\n private cachedZValue: number;\n\n constructor(container: HTMLDivElement) {\n this.container = container;\n this.DOM = { el: container };\n this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img as HTMLDivElement));\n this.imagesTotal = this.images.length;\n this.imgPosition = 0;\n this.zIndexVal = 1;\n this.activeImagesCount = 0;\n this.isIdle = true;\n this.threshold = 80;\n this.mousePos = { x: 0, y: 0 };\n this.lastMousePos = { x: 0, y: 0 };\n this.cacheMousePos = { x: 0, y: 0 };\n this.rotation = { x: 0, y: 0 };\n this.cachedRotation = { x: 0, y: 0 };\n this.zValue = 0;\n this.cachedZValue = 0;\n\n const handlePointerMove = (ev: MouseEvent | TouchEvent) => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n };\n container.addEventListener('mousemove', handlePointerMove);\n container.addEventListener('touchmove', handlePointerMove);\n\n const initRender = (ev: MouseEvent | TouchEvent) => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n this.cacheMousePos = { ...this.mousePos };\n requestAnimationFrame(() => this.render());\n container.removeEventListener('mousemove', initRender as EventListener);\n container.removeEventListener('touchmove', initRender as EventListener);\n };\n container.addEventListener('mousemove', initRender as EventListener);\n container.addEventListener('touchmove', initRender as EventListener);\n }\n\n private render() {\n const distance = getMouseDistance(this.mousePos, this.lastMousePos);\n this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.1);\n this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.1);\n\n if (distance > this.threshold) {\n this.showNextImage();\n this.lastMousePos = { ...this.mousePos };\n }\n if (this.isIdle && this.zIndexVal !== 1) {\n this.zIndexVal = 1;\n }\n requestAnimationFrame(() => this.render());\n }\n\n private showNextImage() {\n const rect = this.container.getBoundingClientRect();\n const centerX = rect.width / 2;\n const centerY = rect.height / 2;\n const relX = this.mousePos.x - centerX;\n const relY = this.mousePos.y - centerY;\n\n this.rotation.x = -(relY / centerY) * 30;\n this.rotation.y = (relX / centerX) * 30;\n this.cachedRotation = { ...this.rotation };\n\n const distanceFromCenter = Math.sqrt(relX * relX + relY * relY);\n const maxDistance = Math.sqrt(centerX * centerX + centerY * centerY);\n const proportion = distanceFromCenter / maxDistance;\n this.zValue = proportion * 1200 - 600;\n this.cachedZValue = this.zValue;\n const normalizedZ = (this.zValue + 600) / 1200;\n const brightness = 0.2 + normalizedZ * 2.3;\n\n ++this.zIndexVal;\n this.imgPosition = this.imgPosition < this.imagesTotal - 1 ? this.imgPosition + 1 : 0;\n const img = this.images[this.imgPosition];\n gsap.killTweensOf(img.DOM.el);\n\n gsap\n .timeline({\n onStart: () => this.onImageActivated(),\n onComplete: () => this.onImageDeactivated()\n })\n .set(this.DOM.el, { perspective: 1000 }, 0)\n .fromTo(\n img.DOM.el,\n {\n opacity: 1,\n z: 0,\n scale: 1 + this.cachedZValue / 1000,\n zIndex: this.zIndexVal,\n x: this.cacheMousePos.x - (img.rect?.width ?? 0) / 2,\n y: this.cacheMousePos.y - (img.rect?.height ?? 0) / 2,\n rotationX: this.cachedRotation.x,\n rotationY: this.cachedRotation.y,\n filter: `brightness(${brightness})`\n },\n {\n duration: 1,\n ease: 'expo',\n scale: 1 + this.zValue / 1000,\n x: this.mousePos.x - (img.rect?.width ?? 0) / 2,\n y: this.mousePos.y - (img.rect?.height ?? 0) / 2,\n rotationX: this.rotation.x,\n rotationY: this.rotation.y\n },\n 0\n )\n .to(\n img.DOM.el,\n {\n duration: 0.4,\n ease: 'power2',\n opacity: 0,\n z: -800\n },\n 0.3\n );\n }\n\n private onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n\n private onImageDeactivated() {\n this.activeImagesCount--;\n if (this.activeImagesCount === 0) {\n this.isIdle = true;\n }\n }\n}\n\ntype ImageTrailConstructor =\n | typeof ImageTrailVariant1\n | typeof ImageTrailVariant2\n | typeof ImageTrailVariant3\n | typeof ImageTrailVariant4\n | typeof ImageTrailVariant5\n | typeof ImageTrailVariant6\n | typeof ImageTrailVariant7\n | typeof ImageTrailVariant8;\n\nconst variantMap: Record<number, ImageTrailConstructor> = {\n 1: ImageTrailVariant1,\n 2: ImageTrailVariant2,\n 3: ImageTrailVariant3,\n 4: ImageTrailVariant4,\n 5: ImageTrailVariant5,\n 6: ImageTrailVariant6,\n 7: ImageTrailVariant7,\n 8: ImageTrailVariant8\n};\n\ninterface ImageTrailProps {\n items?: string[];\n variant?: number;\n}\n\nconst props = withDefaults(defineProps<ImageTrailProps>(), {\n items: () => [],\n variant: 1\n});\n\nconst containerRef = useTemplateRef<HTMLDivElement>('containerRef');\n\nonMounted(async () => {\n await nextTick();\n\n if (!containerRef.value) return;\n\n const Cls = variantMap[props.variant] || variantMap[1];\n new Cls(containerRef.value);\n});\n</script>\n\n<template>\n <div ref=\"containerRef\" class=\"z-[100] relative bg-transparent rounded-lg w-full h-full overflow-visible\">\n <div\n v-for=\"(url, i) in items\"\n :key=\"i\"\n class=\"top-0 left-0 absolute opacity-0 rounded-[15px] w-[190px] aspect-[1.1] overflow-hidden [will-change:transform,filter] content__img\"\n >\n <div\n class=\"top-[-10px] left-[-10px] absolute bg-cover bg-center w-[calc(100%+20px)] h-[calc(100%+20px)] content__img-inner\"\n :style=\"{ backgroundImage: `url(${url})` }\"\n />\n </div>\n </div>\n</template>\n","path":"ImageTrail/ImageTrail.vue","_imports_":[],"registryDependencies":[],"dependencies":[],"devDependencies":[]}],"registryDependencies":[],"dependencies":[{"ecosystem":"js","name":"gsap","version":"^3.13.0"}],"devDependencies":[],"categories":["Animations"]}