diff --git a/public/r/ImageTrail-JS-CSS.json b/public/r/ImageTrail-JS-CSS.json
index 6e4f3b154..db5d08650 100644
--- a/public/r/ImageTrail-JS-CSS.json
+++ b/public/r/ImageTrail-JS-CSS.json
@@ -13,7 +13,7 @@
{
"type": "registry:component",
"path": "ImageTrail/ImageTrail.jsx",
- "content": "import { useRef, useEffect } from 'react';\nimport { gsap } from 'gsap';\n\nimport './ImageTrail.css';\n\nfunction lerp(a, b, n) {\n return (1 - n) * a + n * b;\n}\n\nfunction getLocalPointerPos(e, rect) {\n let clientX = 0,\n clientY = 0;\n if (e.touches && e.touches.length > 0) {\n clientX = e.touches[0].clientX;\n clientY = e.touches[0].clientY;\n } else {\n clientX = e.clientX;\n clientY = e.clientY;\n }\n return {\n x: clientX - rect.left,\n y: clientY - rect.top\n };\n}\nfunction getMouseDistance(p1, p2) {\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 DOM = { el: null, inner: null };\n defaultStyle = { scale: 1, x: 0, y: 0, opacity: 0 };\n rect = null;\n\n constructor(DOM_el) {\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 initEvents() {\n this.resize = () => {\n gsap.set(this.DOM.el, this.defaultStyle);\n this.getRect();\n };\n window.addEventListener('resize', this.resize);\n }\n getRect() {\n this.rect = this.DOM.el.getBoundingClientRect();\n }\n}\n\nclass ImageTrailVariant1 {\n constructor(container) {\n this.container = container;\n this.DOM = { el: container };\n this.images = [...this.DOM.el.querySelectorAll('.content__img')].map(img => new ImageItem(img));\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\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 => {\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 => {\n const rect = this.container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n this.cacheMousePos = { ...this.mousePos };\n\n requestAnimationFrame(() => this.render());\n\n container.removeEventListener('mousemove', initRender);\n container.removeEventListener('touchmove', initRender);\n };\n container.addEventListener('mousemove', initRender);\n container.addEventListener('touchmove', initRender);\n }\n\n render() {\n let 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 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 / 2,\n y: this.cacheMousePos.y - img.rect.height / 2\n },\n {\n duration: 0.4,\n ease: 'power1',\n x: this.mousePos.x - img.rect.width / 2,\n y: this.mousePos.y - img.rect.height / 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 onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n onImageDeactivated() {\n this.activeImagesCount--;\n if (this.activeImagesCount === 0) {\n this.isIdle = true;\n }\n }\n}\n\nclass ImageTrailVariant2 {\n constructor(container) {\n this.container = container;\n this.DOM = { el: container };\n this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img));\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\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 => {\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 => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n this.cacheMousePos = { ...this.mousePos };\n\n requestAnimationFrame(() => this.render());\n\n container.removeEventListener('mousemove', initRender);\n container.removeEventListener('touchmove', initRender);\n };\n container.addEventListener('mousemove', initRender);\n container.addEventListener('touchmove', initRender);\n }\n\n render() {\n let 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 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 / 2,\n y: this.cacheMousePos.y - img.rect.height / 2\n },\n {\n duration: 0.4,\n ease: 'power1',\n scale: 1,\n x: this.mousePos.x - img.rect.width / 2,\n y: this.mousePos.y - img.rect.height / 2\n },\n 0\n )\n .fromTo(\n img.DOM.inner,\n {\n scale: 2.8,\n filter: 'brightness(250%)'\n },\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 onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n onImageDeactivated() {\n this.activeImagesCount--;\n if (this.activeImagesCount === 0) this.isIdle = true;\n }\n}\n\nclass ImageTrailVariant3 {\n constructor(container) {\n this.container = container;\n this.DOM = { el: container };\n this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img));\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\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 => {\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 => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n this.cacheMousePos = { ...this.mousePos };\n\n requestAnimationFrame(() => this.render());\n container.removeEventListener('mousemove', initRender);\n container.removeEventListener('touchmove', initRender);\n };\n container.addEventListener('mousemove', initRender);\n container.addEventListener('touchmove', initRender);\n }\n\n render() {\n let 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 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 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 / 2,\n y: this.cacheMousePos.y - img.rect.height / 2\n },\n {\n duration: 0.4,\n ease: 'power1',\n scale: 1,\n x: this.mousePos.x - img.rect.width / 2,\n y: this.mousePos.y - img.rect.height / 2\n },\n 0\n )\n .fromTo(\n img.DOM.inner,\n {\n scale: 1.2\n },\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 onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n onImageDeactivated() {\n this.activeImagesCount--;\n if (this.activeImagesCount === 0) this.isIdle = true;\n }\n}\n\nclass ImageTrailVariant4 {\n constructor(container) {\n this.container = container;\n this.DOM = { el: container };\n this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img));\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\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 => {\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 => {\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);\n container.removeEventListener('touchmove', initRender);\n };\n container.addEventListener('mousemove', initRender);\n container.addEventListener('touchmove', initRender);\n }\n\n render() {\n let 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 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 let 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 / 2,\n y: this.cacheMousePos.y - img.rect.height / 2\n },\n {\n duration: 0.4,\n ease: 'power1',\n scale: 1,\n x: this.mousePos.x - img.rect.width / 2,\n y: this.mousePos.y - img.rect.height / 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((400 * distance) / 100, 100)}%)`\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 onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n onImageDeactivated() {\n this.activeImagesCount--;\n if (this.activeImagesCount === 0) this.isIdle = true;\n }\n}\n\nclass ImageTrailVariant5 {\n constructor(container) {\n this.container = container;\n this.DOM = { el: container };\n this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img));\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\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 => {\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 => {\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);\n container.removeEventListener('touchmove', initRender);\n };\n container.addEventListener('mousemove', initRender);\n container.addEventListener('touchmove', initRender);\n }\n\n render() {\n let 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 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 let startAngle = isMovingClockwise ? angle - 10 : angle + 10;\n let 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 / 2,\n y: this.cacheMousePos.y - img.rect.height / 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 / 2 + dx * 70,\n y: this.mousePos.y - img.rect.height / 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 onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n onImageDeactivated() {\n this.activeImagesCount--;\n if (this.activeImagesCount === 0) this.isIdle = true;\n }\n}\n\nclass ImageTrailVariant6 {\n constructor(container) {\n this.container = container;\n this.DOM = { el: container };\n this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img));\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\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 => {\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 => {\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);\n container.removeEventListener('touchmove', initRender);\n };\n container.addEventListener('mousemove', initRender);\n container.addEventListener('touchmove', initRender);\n }\n\n render() {\n let 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 mapSpeedToSize(speed, minSize, maxSize) {\n const maxSpeed = 200;\n return minSize + (maxSize - minSize) * Math.min(speed / maxSpeed, 1);\n }\n mapSpeedToBrightness(speed, minB, maxB) {\n const maxSpeed = 70;\n return minB + (maxB - minB) * Math.min(speed / maxSpeed, 1);\n }\n mapSpeedToBlur(speed, minBlur, maxBlur) {\n const maxSpeed = 90;\n return minBlur + (maxBlur - minBlur) * Math.min(speed / maxSpeed, 1);\n }\n mapSpeedToGrayscale(speed, minG, maxG) {\n const maxSpeed = 90;\n return minG + (maxG - minG) * Math.min(speed / maxSpeed, 1);\n }\n\n showNextImage() {\n let dx = this.mousePos.x - this.cacheMousePos.x;\n let dy = this.mousePos.y - this.cacheMousePos.y;\n let 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 let scaleFactor = this.mapSpeedToSize(speed, 0.3, 2);\n let brightnessValue = this.mapSpeedToBrightness(speed, 0, 1.3);\n let blurValue = this.mapSpeedToBlur(speed, 20, 0);\n let 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 / 2,\n y: this.cacheMousePos.y - img.rect.height / 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 / 2,\n y: this.mousePos.y - img.rect.height / 2\n },\n 0\n )\n .fromTo(\n img.DOM.inner,\n {\n scale: 2\n },\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 onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n onImageDeactivated() {\n this.activeImagesCount--;\n if (this.activeImagesCount === 0) {\n this.isIdle = true;\n }\n }\n}\n\nfunction getNewPosition(position, offset, arr) {\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}\nclass ImageTrailVariant7 {\n constructor(container) {\n this.container = container;\n this.DOM = { el: container };\n this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img));\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\n this.mousePos = { x: 0, y: 0 };\n this.lastMousePos = { x: 0, y: 0 };\n this.cacheMousePos = { x: 0, y: 0 };\n\n this.visibleImagesCount = 0;\n this.visibleImagesTotal = 9;\n this.visibleImagesTotal = Math.min(this.visibleImagesTotal, this.imagesTotal - 1);\n\n const handlePointerMove = ev => {\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 => {\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);\n container.removeEventListener('touchmove', initRender);\n };\n container.addEventListener('mousemove', initRender);\n container.addEventListener('touchmove', initRender);\n }\n\n render() {\n let 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 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 / 2,\n y: this.cacheMousePos.y - img.rect.height / 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 / 2,\n y: this.mousePos.y - img.rect.height / 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 onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n onImageDeactivated() {\n this.activeImagesCount--;\n }\n}\n\nclass ImageTrailVariant8 {\n constructor(container) {\n this.container = container;\n this.DOM = { el: container };\n this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img));\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\n this.mousePos = { x: 0, y: 0 };\n this.lastMousePos = { x: 0, y: 0 };\n this.cacheMousePos = { x: 0, y: 0 };\n\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 => {\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 => {\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);\n container.removeEventListener('touchmove', initRender);\n };\n container.addEventListener('mousemove', initRender);\n container.addEventListener('touchmove', initRender);\n }\n\n render() {\n let 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 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 / 2,\n y: this.cacheMousePos.y - img.rect.height / 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 / 2,\n y: this.mousePos.y - img.rect.height / 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 onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n onImageDeactivated() {\n this.activeImagesCount--;\n if (this.activeImagesCount === 0) this.isIdle = true;\n }\n}\n\nconst variantMap = {\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\nexport default function ImageTrail({ items = [], variant = 1 }) {\n const containerRef = useRef(null);\n\n useEffect(() => {\n if (!containerRef.current) return;\n\n const Cls = variantMap[variant] || variantMap[1];\n new Cls(containerRef.current);\n }, [variant, items]);\n\n return (\n
\n {items.map((url, i) => (\n
\n ))}\n
\n );\n}\n"
+ "content": "import { useRef, useEffect } from 'react';\nimport { gsap } from 'gsap';\n\nimport './ImageTrail.css';\n\nfunction lerp(a, b, n) {\n return (1 - n) * a + n * b;\n}\n\nfunction getLocalPointerPos(e, rect) {\n let clientX = 0,\n clientY = 0;\n if (e.touches && e.touches.length > 0) {\n clientX = e.touches[0].clientX;\n clientY = e.touches[0].clientY;\n } else {\n clientX = e.clientX;\n clientY = e.clientY;\n }\n return {\n x: clientX - rect.left,\n y: clientY - rect.top\n };\n}\nfunction getMouseDistance(p1, p2) {\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 DOM = { el: null, inner: null };\n defaultStyle = { scale: 1, x: 0, y: 0, opacity: 0 };\n rect = null;\n\n constructor(DOM_el) {\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 initEvents() {\n this.resize = () => {\n gsap.set(this.DOM.el, this.defaultStyle);\n this.getRect();\n };\n window.addEventListener('resize', this.resize);\n }\n getRect() {\n this.rect = this.DOM.el.getBoundingClientRect();\n }\n\n destroy() {\n window.removeEventListener('resize', this.resize);\n }\n}\n\nclass ImageTrailVariant1 {\n constructor(container) {\n this.container = container;\n this.rafId = null;\n this.destroyed = false;\n this.DOM = { el: container };\n this.images = [...this.DOM.el.querySelectorAll('.content__img')].map(img => new ImageItem(img));\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\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 => {\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 => {\n const rect = this.container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n this.cacheMousePos = { ...this.mousePos };\n\n this.rafId = requestAnimationFrame(() => this.render());\n\n container.removeEventListener('mousemove', initRender);\n container.removeEventListener('touchmove', initRender);\n };\n container.addEventListener('mousemove', initRender);\n container.addEventListener('touchmove', initRender);\n this.handlePointerMove = handlePointerMove;\n this.initRender = initRender;\n }\n\n render() {\n if (this.destroyed) return;\n\n let 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 this.rafId = requestAnimationFrame(() => this.render());\n }\n\n 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 / 2,\n y: this.cacheMousePos.y - img.rect.height / 2\n },\n {\n duration: 0.4,\n ease: 'power1',\n x: this.mousePos.x - img.rect.width / 2,\n y: this.mousePos.y - img.rect.height / 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 destroy() {\n this.destroyed = true;\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n this.container.removeEventListener('mousemove', this.handlePointerMove);\n this.container.removeEventListener('touchmove', this.handlePointerMove);\n this.container.removeEventListener('mousemove', this.initRender);\n this.container.removeEventListener('touchmove', this.initRender);\n this.images.forEach(img => {\n gsap.killTweensOf(img.DOM.el);\n img.destroy();\n });\n }\n\n onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n onImageDeactivated() {\n this.activeImagesCount--;\n if (this.activeImagesCount === 0) {\n this.isIdle = true;\n }\n }\n}\n\nclass ImageTrailVariant2 {\n constructor(container) {\n this.container = container;\n this.rafId = null;\n this.destroyed = false;\n this.DOM = { el: container };\n this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img));\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\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 => {\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 => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n this.cacheMousePos = { ...this.mousePos };\n\n this.rafId = requestAnimationFrame(() => this.render());\n\n container.removeEventListener('mousemove', initRender);\n container.removeEventListener('touchmove', initRender);\n };\n container.addEventListener('mousemove', initRender);\n container.addEventListener('touchmove', initRender);\n this.handlePointerMove = handlePointerMove;\n this.initRender = initRender;\n }\n\n render() {\n if (this.destroyed) return;\n\n let 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 this.rafId = requestAnimationFrame(() => this.render());\n }\n\n 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 / 2,\n y: this.cacheMousePos.y - img.rect.height / 2\n },\n {\n duration: 0.4,\n ease: 'power1',\n scale: 1,\n x: this.mousePos.x - img.rect.width / 2,\n y: this.mousePos.y - img.rect.height / 2\n },\n 0\n )\n .fromTo(\n img.DOM.inner,\n {\n scale: 2.8,\n filter: 'brightness(250%)'\n },\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 destroy() {\n this.destroyed = true;\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n this.container.removeEventListener('mousemove', this.handlePointerMove);\n this.container.removeEventListener('touchmove', this.handlePointerMove);\n this.container.removeEventListener('mousemove', this.initRender);\n this.container.removeEventListener('touchmove', this.initRender);\n this.images.forEach(img => {\n gsap.killTweensOf(img.DOM.el);\n img.destroy();\n });\n }\n\n onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n onImageDeactivated() {\n this.activeImagesCount--;\n if (this.activeImagesCount === 0) this.isIdle = true;\n }\n}\n\nclass ImageTrailVariant3 {\n constructor(container) {\n this.container = container;\n this.rafId = null;\n this.destroyed = false;\n this.DOM = { el: container };\n this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img));\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\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 => {\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 => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n this.cacheMousePos = { ...this.mousePos };\n\n this.rafId = requestAnimationFrame(() => this.render());\n container.removeEventListener('mousemove', initRender);\n container.removeEventListener('touchmove', initRender);\n };\n container.addEventListener('mousemove', initRender);\n container.addEventListener('touchmove', initRender);\n this.handlePointerMove = handlePointerMove;\n this.initRender = initRender;\n }\n\n render() {\n if (this.destroyed) return;\n\n let 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 this.rafId = requestAnimationFrame(() => this.render());\n }\n\n 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 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 / 2,\n y: this.cacheMousePos.y - img.rect.height / 2\n },\n {\n duration: 0.4,\n ease: 'power1',\n scale: 1,\n x: this.mousePos.x - img.rect.width / 2,\n y: this.mousePos.y - img.rect.height / 2\n },\n 0\n )\n .fromTo(\n img.DOM.inner,\n {\n scale: 1.2\n },\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 destroy() {\n this.destroyed = true;\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n this.container.removeEventListener('mousemove', this.handlePointerMove);\n this.container.removeEventListener('touchmove', this.handlePointerMove);\n this.container.removeEventListener('mousemove', this.initRender);\n this.container.removeEventListener('touchmove', this.initRender);\n this.images.forEach(img => {\n gsap.killTweensOf(img.DOM.el);\n img.destroy();\n });\n }\n\n onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n onImageDeactivated() {\n this.activeImagesCount--;\n if (this.activeImagesCount === 0) this.isIdle = true;\n }\n}\n\nclass ImageTrailVariant4 {\n constructor(container) {\n this.container = container;\n this.rafId = null;\n this.destroyed = false;\n this.DOM = { el: container };\n this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img));\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\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 => {\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 => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n this.cacheMousePos = { ...this.mousePos };\n this.rafId = requestAnimationFrame(() => this.render());\n container.removeEventListener('mousemove', initRender);\n container.removeEventListener('touchmove', initRender);\n };\n container.addEventListener('mousemove', initRender);\n container.addEventListener('touchmove', initRender);\n this.handlePointerMove = handlePointerMove;\n this.initRender = initRender;\n }\n\n render() {\n if (this.destroyed) return;\n\n let 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 this.rafId = requestAnimationFrame(() => this.render());\n }\n\n 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 let 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 / 2,\n y: this.cacheMousePos.y - img.rect.height / 2\n },\n {\n duration: 0.4,\n ease: 'power1',\n scale: 1,\n x: this.mousePos.x - img.rect.width / 2,\n y: this.mousePos.y - img.rect.height / 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((400 * distance) / 100, 100)}%)`\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 destroy() {\n this.destroyed = true;\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n this.container.removeEventListener('mousemove', this.handlePointerMove);\n this.container.removeEventListener('touchmove', this.handlePointerMove);\n this.container.removeEventListener('mousemove', this.initRender);\n this.container.removeEventListener('touchmove', this.initRender);\n this.images.forEach(img => {\n gsap.killTweensOf(img.DOM.el);\n img.destroy();\n });\n }\n\n onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n onImageDeactivated() {\n this.activeImagesCount--;\n if (this.activeImagesCount === 0) this.isIdle = true;\n }\n}\n\nclass ImageTrailVariant5 {\n constructor(container) {\n this.container = container;\n this.rafId = null;\n this.destroyed = false;\n this.DOM = { el: container };\n this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img));\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\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 => {\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 => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n this.cacheMousePos = { ...this.mousePos };\n this.rafId = requestAnimationFrame(() => this.render());\n container.removeEventListener('mousemove', initRender);\n container.removeEventListener('touchmove', initRender);\n };\n container.addEventListener('mousemove', initRender);\n container.addEventListener('touchmove', initRender);\n this.handlePointerMove = handlePointerMove;\n this.initRender = initRender;\n }\n\n render() {\n if (this.destroyed) return;\n\n let 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 this.rafId = requestAnimationFrame(() => this.render());\n }\n\n 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 let startAngle = isMovingClockwise ? angle - 10 : angle + 10;\n let 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 / 2,\n y: this.cacheMousePos.y - img.rect.height / 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 / 2 + dx * 70,\n y: this.mousePos.y - img.rect.height / 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 destroy() {\n this.destroyed = true;\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n this.container.removeEventListener('mousemove', this.handlePointerMove);\n this.container.removeEventListener('touchmove', this.handlePointerMove);\n this.container.removeEventListener('mousemove', this.initRender);\n this.container.removeEventListener('touchmove', this.initRender);\n this.images.forEach(img => {\n gsap.killTweensOf(img.DOM.el);\n img.destroy();\n });\n }\n\n onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n onImageDeactivated() {\n this.activeImagesCount--;\n if (this.activeImagesCount === 0) this.isIdle = true;\n }\n}\n\nclass ImageTrailVariant6 {\n constructor(container) {\n this.container = container;\n this.rafId = null;\n this.destroyed = false;\n this.DOM = { el: container };\n this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img));\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\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 => {\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 => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n this.cacheMousePos = { ...this.mousePos };\n this.rafId = requestAnimationFrame(() => this.render());\n container.removeEventListener('mousemove', initRender);\n container.removeEventListener('touchmove', initRender);\n };\n container.addEventListener('mousemove', initRender);\n container.addEventListener('touchmove', initRender);\n this.handlePointerMove = handlePointerMove;\n this.initRender = initRender;\n }\n\n render() {\n if (this.destroyed) return;\n\n let 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 this.rafId = requestAnimationFrame(() => this.render());\n }\n\n mapSpeedToSize(speed, minSize, maxSize) {\n const maxSpeed = 200;\n return minSize + (maxSize - minSize) * Math.min(speed / maxSpeed, 1);\n }\n mapSpeedToBrightness(speed, minB, maxB) {\n const maxSpeed = 70;\n return minB + (maxB - minB) * Math.min(speed / maxSpeed, 1);\n }\n mapSpeedToBlur(speed, minBlur, maxBlur) {\n const maxSpeed = 90;\n return minBlur + (maxBlur - minBlur) * Math.min(speed / maxSpeed, 1);\n }\n mapSpeedToGrayscale(speed, minG, maxG) {\n const maxSpeed = 90;\n return minG + (maxG - minG) * Math.min(speed / maxSpeed, 1);\n }\n\n showNextImage() {\n let dx = this.mousePos.x - this.cacheMousePos.x;\n let dy = this.mousePos.y - this.cacheMousePos.y;\n let 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 let scaleFactor = this.mapSpeedToSize(speed, 0.3, 2);\n let brightnessValue = this.mapSpeedToBrightness(speed, 0, 1.3);\n let blurValue = this.mapSpeedToBlur(speed, 20, 0);\n let 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 / 2,\n y: this.cacheMousePos.y - img.rect.height / 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 / 2,\n y: this.mousePos.y - img.rect.height / 2\n },\n 0\n )\n .fromTo(\n img.DOM.inner,\n {\n scale: 2\n },\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 destroy() {\n this.destroyed = true;\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n this.container.removeEventListener('mousemove', this.handlePointerMove);\n this.container.removeEventListener('touchmove', this.handlePointerMove);\n this.container.removeEventListener('mousemove', this.initRender);\n this.container.removeEventListener('touchmove', this.initRender);\n this.images.forEach(img => {\n gsap.killTweensOf(img.DOM.el);\n img.destroy();\n });\n }\n\n onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n onImageDeactivated() {\n this.activeImagesCount--;\n if (this.activeImagesCount === 0) {\n this.isIdle = true;\n }\n }\n}\n\nfunction getNewPosition(position, offset, arr) {\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}\nclass ImageTrailVariant7 {\n constructor(container) {\n this.container = container;\n this.rafId = null;\n this.destroyed = false;\n this.DOM = { el: container };\n this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img));\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\n this.mousePos = { x: 0, y: 0 };\n this.lastMousePos = { x: 0, y: 0 };\n this.cacheMousePos = { x: 0, y: 0 };\n\n this.visibleImagesCount = 0;\n this.visibleImagesTotal = 9;\n this.visibleImagesTotal = Math.min(this.visibleImagesTotal, this.imagesTotal - 1);\n\n const handlePointerMove = ev => {\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 => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n this.cacheMousePos = { ...this.mousePos };\n this.rafId = requestAnimationFrame(() => this.render());\n container.removeEventListener('mousemove', initRender);\n container.removeEventListener('touchmove', initRender);\n };\n container.addEventListener('mousemove', initRender);\n container.addEventListener('touchmove', initRender);\n this.handlePointerMove = handlePointerMove;\n this.initRender = initRender;\n }\n\n render() {\n if (this.destroyed) return;\n\n let 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 this.rafId = requestAnimationFrame(() => this.render());\n }\n\n 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 / 2,\n y: this.cacheMousePos.y - img.rect.height / 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 / 2,\n y: this.mousePos.y - img.rect.height / 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 destroy() {\n this.destroyed = true;\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n this.container.removeEventListener('mousemove', this.handlePointerMove);\n this.container.removeEventListener('touchmove', this.handlePointerMove);\n this.container.removeEventListener('mousemove', this.initRender);\n this.container.removeEventListener('touchmove', this.initRender);\n this.images.forEach(img => {\n gsap.killTweensOf(img.DOM.el);\n img.destroy();\n });\n }\n\n onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n onImageDeactivated() {\n this.activeImagesCount--;\n }\n}\n\nclass ImageTrailVariant8 {\n constructor(container) {\n this.container = container;\n this.rafId = null;\n this.destroyed = false;\n this.DOM = { el: container };\n this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img));\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\n this.mousePos = { x: 0, y: 0 };\n this.lastMousePos = { x: 0, y: 0 };\n this.cacheMousePos = { x: 0, y: 0 };\n\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 => {\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 => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n this.cacheMousePos = { ...this.mousePos };\n this.rafId = requestAnimationFrame(() => this.render());\n container.removeEventListener('mousemove', initRender);\n container.removeEventListener('touchmove', initRender);\n };\n container.addEventListener('mousemove', initRender);\n container.addEventListener('touchmove', initRender);\n this.handlePointerMove = handlePointerMove;\n this.initRender = initRender;\n }\n\n render() {\n if (this.destroyed) return;\n\n let 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 this.rafId = requestAnimationFrame(() => this.render());\n }\n\n 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 / 2,\n y: this.cacheMousePos.y - img.rect.height / 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 / 2,\n y: this.mousePos.y - img.rect.height / 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 destroy() {\n this.destroyed = true;\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n this.container.removeEventListener('mousemove', this.handlePointerMove);\n this.container.removeEventListener('touchmove', this.handlePointerMove);\n this.container.removeEventListener('mousemove', this.initRender);\n this.container.removeEventListener('touchmove', this.initRender);\n this.images.forEach(img => {\n gsap.killTweensOf(img.DOM.el);\n img.destroy();\n });\n }\n\n onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n onImageDeactivated() {\n this.activeImagesCount--;\n if (this.activeImagesCount === 0) this.isIdle = true;\n }\n}\n\nconst variantMap = {\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\nexport default function ImageTrail({ items = [], variant = 1 }) {\n const containerRef = useRef(null);\n\n useEffect(() => {\n if (!containerRef.current) return;\n\n const Cls = variantMap[variant] || variantMap[1];\n const instance = new Cls(containerRef.current);\n\n return () => {\n instance.destroy();\n };\n }, [variant, items]);\n\n return (\n \n {items.map((url, i) => (\n
\n ))}\n
\n );\n}\n"
}
],
"registryDependencies": [],
diff --git a/public/r/ImageTrail-JS-TW.json b/public/r/ImageTrail-JS-TW.json
index 94c95d5be..d12986620 100644
--- a/public/r/ImageTrail-JS-TW.json
+++ b/public/r/ImageTrail-JS-TW.json
@@ -8,7 +8,7 @@
{
"type": "registry:component",
"path": "ImageTrail/ImageTrail.jsx",
- "content": "import { useRef, useEffect } from 'react';\nimport { gsap } from 'gsap';\n\nfunction lerp(a, b, n) {\n return (1 - n) * a + n * b;\n}\n\nfunction getLocalPointerPos(e, rect) {\n let clientX = 0,\n clientY = 0;\n if (e.touches && e.touches.length > 0) {\n clientX = e.touches[0].clientX;\n clientY = e.touches[0].clientY;\n } else {\n clientX = e.clientX;\n clientY = e.clientY;\n }\n return {\n x: clientX - rect.left,\n y: clientY - rect.top\n };\n}\nfunction getMouseDistance(p1, p2) {\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 DOM = { el: null, inner: null };\n defaultStyle = { scale: 1, x: 0, y: 0, opacity: 0 };\n rect = null;\n\n constructor(DOM_el) {\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 initEvents() {\n this.resize = () => {\n gsap.set(this.DOM.el, this.defaultStyle);\n this.getRect();\n };\n window.addEventListener('resize', this.resize);\n }\n getRect() {\n this.rect = this.DOM.el.getBoundingClientRect();\n }\n}\n\nclass ImageTrailVariant1 {\n constructor(container) {\n this.container = container;\n this.DOM = { el: container };\n this.images = [...this.DOM.el.querySelectorAll('.content__img')].map(img => new ImageItem(img));\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\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 => {\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 => {\n const rect = this.container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n this.cacheMousePos = { ...this.mousePos };\n\n requestAnimationFrame(() => this.render());\n\n container.removeEventListener('mousemove', initRender);\n container.removeEventListener('touchmove', initRender);\n };\n container.addEventListener('mousemove', initRender);\n container.addEventListener('touchmove', initRender);\n }\n\n render() {\n let 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 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 / 2,\n y: this.cacheMousePos.y - img.rect.height / 2\n },\n {\n duration: 0.4,\n ease: 'power1',\n x: this.mousePos.x - img.rect.width / 2,\n y: this.mousePos.y - img.rect.height / 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 onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n onImageDeactivated() {\n this.activeImagesCount--;\n if (this.activeImagesCount === 0) {\n this.isIdle = true;\n }\n }\n}\n\nclass ImageTrailVariant2 {\n constructor(container) {\n this.container = container;\n this.DOM = { el: container };\n this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img));\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\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 => {\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 => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n this.cacheMousePos = { ...this.mousePos };\n\n requestAnimationFrame(() => this.render());\n\n container.removeEventListener('mousemove', initRender);\n container.removeEventListener('touchmove', initRender);\n };\n container.addEventListener('mousemove', initRender);\n container.addEventListener('touchmove', initRender);\n }\n\n render() {\n let 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 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 / 2,\n y: this.cacheMousePos.y - img.rect.height / 2\n },\n {\n duration: 0.4,\n ease: 'power1',\n scale: 1,\n x: this.mousePos.x - img.rect.width / 2,\n y: this.mousePos.y - img.rect.height / 2\n },\n 0\n )\n .fromTo(\n img.DOM.inner,\n {\n scale: 2.8,\n filter: 'brightness(250%)'\n },\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 onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n onImageDeactivated() {\n this.activeImagesCount--;\n if (this.activeImagesCount === 0) this.isIdle = true;\n }\n}\n\nclass ImageTrailVariant3 {\n constructor(container) {\n this.container = container;\n this.DOM = { el: container };\n this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img));\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\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 => {\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 => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n this.cacheMousePos = { ...this.mousePos };\n\n requestAnimationFrame(() => this.render());\n container.removeEventListener('mousemove', initRender);\n container.removeEventListener('touchmove', initRender);\n };\n container.addEventListener('mousemove', initRender);\n container.addEventListener('touchmove', initRender);\n }\n\n render() {\n let 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 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 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 / 2,\n y: this.cacheMousePos.y - img.rect.height / 2\n },\n {\n duration: 0.4,\n ease: 'power1',\n scale: 1,\n x: this.mousePos.x - img.rect.width / 2,\n y: this.mousePos.y - img.rect.height / 2\n },\n 0\n )\n .fromTo(\n img.DOM.inner,\n {\n scale: 1.2\n },\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 onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n onImageDeactivated() {\n this.activeImagesCount--;\n if (this.activeImagesCount === 0) this.isIdle = true;\n }\n}\n\nclass ImageTrailVariant4 {\n constructor(container) {\n this.container = container;\n this.DOM = { el: container };\n this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img));\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\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 => {\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 => {\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);\n container.removeEventListener('touchmove', initRender);\n };\n container.addEventListener('mousemove', initRender);\n container.addEventListener('touchmove', initRender);\n }\n\n render() {\n let 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 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 let 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 / 2,\n y: this.cacheMousePos.y - img.rect.height / 2\n },\n {\n duration: 0.4,\n ease: 'power1',\n scale: 1,\n x: this.mousePos.x - img.rect.width / 2,\n y: this.mousePos.y - img.rect.height / 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((400 * distance) / 100, 100)}%)`\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 onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n onImageDeactivated() {\n this.activeImagesCount--;\n if (this.activeImagesCount === 0) this.isIdle = true;\n }\n}\n\nclass ImageTrailVariant5 {\n constructor(container) {\n this.container = container;\n this.DOM = { el: container };\n this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img));\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\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 => {\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 => {\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);\n container.removeEventListener('touchmove', initRender);\n };\n container.addEventListener('mousemove', initRender);\n container.addEventListener('touchmove', initRender);\n }\n\n render() {\n let 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 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 let startAngle = isMovingClockwise ? angle - 10 : angle + 10;\n let 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 / 2,\n y: this.cacheMousePos.y - img.rect.height / 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 / 2 + dx * 70,\n y: this.mousePos.y - img.rect.height / 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 onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n onImageDeactivated() {\n this.activeImagesCount--;\n if (this.activeImagesCount === 0) this.isIdle = true;\n }\n}\n\nclass ImageTrailVariant6 {\n constructor(container) {\n this.container = container;\n this.DOM = { el: container };\n this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img));\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\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 => {\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 => {\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);\n container.removeEventListener('touchmove', initRender);\n };\n container.addEventListener('mousemove', initRender);\n container.addEventListener('touchmove', initRender);\n }\n\n render() {\n let 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 mapSpeedToSize(speed, minSize, maxSize) {\n const maxSpeed = 200;\n return minSize + (maxSize - minSize) * Math.min(speed / maxSpeed, 1);\n }\n mapSpeedToBrightness(speed, minB, maxB) {\n const maxSpeed = 70;\n return minB + (maxB - minB) * Math.min(speed / maxSpeed, 1);\n }\n mapSpeedToBlur(speed, minBlur, maxBlur) {\n const maxSpeed = 90;\n return minBlur + (maxBlur - minBlur) * Math.min(speed / maxSpeed, 1);\n }\n mapSpeedToGrayscale(speed, minG, maxG) {\n const maxSpeed = 90;\n return minG + (maxG - minG) * Math.min(speed / maxSpeed, 1);\n }\n\n showNextImage() {\n let dx = this.mousePos.x - this.cacheMousePos.x;\n let dy = this.mousePos.y - this.cacheMousePos.y;\n let 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 let scaleFactor = this.mapSpeedToSize(speed, 0.3, 2);\n let brightnessValue = this.mapSpeedToBrightness(speed, 0, 1.3);\n let blurValue = this.mapSpeedToBlur(speed, 20, 0);\n let 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 / 2,\n y: this.cacheMousePos.y - img.rect.height / 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 / 2,\n y: this.mousePos.y - img.rect.height / 2\n },\n 0\n )\n .fromTo(\n img.DOM.inner,\n {\n scale: 2\n },\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 onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n onImageDeactivated() {\n this.activeImagesCount--;\n if (this.activeImagesCount === 0) {\n this.isIdle = true;\n }\n }\n}\n\nfunction getNewPosition(position, offset, arr) {\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}\nclass ImageTrailVariant7 {\n constructor(container) {\n this.container = container;\n this.DOM = { el: container };\n this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img));\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\n this.mousePos = { x: 0, y: 0 };\n this.lastMousePos = { x: 0, y: 0 };\n this.cacheMousePos = { x: 0, y: 0 };\n\n this.visibleImagesCount = 0;\n this.visibleImagesTotal = 9;\n this.visibleImagesTotal = Math.min(this.visibleImagesTotal, this.imagesTotal - 1);\n\n const handlePointerMove = ev => {\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 => {\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);\n container.removeEventListener('touchmove', initRender);\n };\n container.addEventListener('mousemove', initRender);\n container.addEventListener('touchmove', initRender);\n }\n\n render() {\n let 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 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 / 2,\n y: this.cacheMousePos.y - img.rect.height / 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 / 2,\n y: this.mousePos.y - img.rect.height / 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 onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n onImageDeactivated() {\n this.activeImagesCount--;\n }\n}\n\nclass ImageTrailVariant8 {\n constructor(container) {\n this.container = container;\n this.DOM = { el: container };\n this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img));\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\n this.mousePos = { x: 0, y: 0 };\n this.lastMousePos = { x: 0, y: 0 };\n this.cacheMousePos = { x: 0, y: 0 };\n\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 => {\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 => {\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);\n container.removeEventListener('touchmove', initRender);\n };\n container.addEventListener('mousemove', initRender);\n container.addEventListener('touchmove', initRender);\n }\n\n render() {\n let 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 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 / 2,\n y: this.cacheMousePos.y - img.rect.height / 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 / 2,\n y: this.mousePos.y - img.rect.height / 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 onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n onImageDeactivated() {\n this.activeImagesCount--;\n if (this.activeImagesCount === 0) this.isIdle = true;\n }\n}\n\nconst variantMap = {\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\nexport default function ImageTrail({ items = [], variant = 1 }) {\n const containerRef = useRef(null);\n\n useEffect(() => {\n if (!containerRef.current) return;\n\n const Cls = variantMap[variant] || variantMap[1];\n new Cls(containerRef.current);\n }, [variant, items]);\n\n return (\n \n {items.map((url, i) => (\n
\n ))}\n
\n );\n}\n"
+ "content": "import { useRef, useEffect } from 'react';\nimport { gsap } from 'gsap';\n\nfunction lerp(a, b, n) {\n return (1 - n) * a + n * b;\n}\n\nfunction getLocalPointerPos(e, rect) {\n let clientX = 0,\n clientY = 0;\n if (e.touches && e.touches.length > 0) {\n clientX = e.touches[0].clientX;\n clientY = e.touches[0].clientY;\n } else {\n clientX = e.clientX;\n clientY = e.clientY;\n }\n return {\n x: clientX - rect.left,\n y: clientY - rect.top\n };\n}\nfunction getMouseDistance(p1, p2) {\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 DOM = { el: null, inner: null };\n defaultStyle = { scale: 1, x: 0, y: 0, opacity: 0 };\n rect = null;\n\n constructor(DOM_el) {\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 initEvents() {\n this.resize = () => {\n gsap.set(this.DOM.el, this.defaultStyle);\n this.getRect();\n };\n window.addEventListener('resize', this.resize);\n }\n getRect() {\n this.rect = this.DOM.el.getBoundingClientRect();\n }\n\n destroy() {\n window.removeEventListener('resize', this.resize);\n }\n}\n\nclass ImageTrailVariant1 {\n constructor(container) {\n this.container = container;\n this.rafId = null;\n this.destroyed = false;\n this.DOM = { el: container };\n this.images = [...this.DOM.el.querySelectorAll('.content__img')].map(img => new ImageItem(img));\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\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 => {\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 => {\n const rect = this.container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n this.cacheMousePos = { ...this.mousePos };\n\n this.rafId = requestAnimationFrame(() => this.render());\n\n container.removeEventListener('mousemove', initRender);\n container.removeEventListener('touchmove', initRender);\n };\n container.addEventListener('mousemove', initRender);\n container.addEventListener('touchmove', initRender);\n this.handlePointerMove = handlePointerMove;\n this.initRender = initRender;\n }\n\n render() {\n if (this.destroyed) return;\n\n let 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 this.rafId = requestAnimationFrame(() => this.render());\n }\n\n 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 / 2,\n y: this.cacheMousePos.y - img.rect.height / 2\n },\n {\n duration: 0.4,\n ease: 'power1',\n x: this.mousePos.x - img.rect.width / 2,\n y: this.mousePos.y - img.rect.height / 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 destroy() {\n this.destroyed = true;\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n this.container.removeEventListener('mousemove', this.handlePointerMove);\n this.container.removeEventListener('touchmove', this.handlePointerMove);\n this.container.removeEventListener('mousemove', this.initRender);\n this.container.removeEventListener('touchmove', this.initRender);\n this.images.forEach(img => {\n gsap.killTweensOf(img.DOM.el);\n img.destroy();\n });\n }\n\n onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n onImageDeactivated() {\n this.activeImagesCount--;\n if (this.activeImagesCount === 0) {\n this.isIdle = true;\n }\n }\n}\n\nclass ImageTrailVariant2 {\n constructor(container) {\n this.container = container;\n this.rafId = null;\n this.destroyed = false;\n this.DOM = { el: container };\n this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img));\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\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 => {\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 => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n this.cacheMousePos = { ...this.mousePos };\n\n this.rafId = requestAnimationFrame(() => this.render());\n\n container.removeEventListener('mousemove', initRender);\n container.removeEventListener('touchmove', initRender);\n };\n container.addEventListener('mousemove', initRender);\n container.addEventListener('touchmove', initRender);\n this.handlePointerMove = handlePointerMove;\n this.initRender = initRender;\n }\n\n render() {\n if (this.destroyed) return;\n\n let 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 this.rafId = requestAnimationFrame(() => this.render());\n }\n\n 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 / 2,\n y: this.cacheMousePos.y - img.rect.height / 2\n },\n {\n duration: 0.4,\n ease: 'power1',\n scale: 1,\n x: this.mousePos.x - img.rect.width / 2,\n y: this.mousePos.y - img.rect.height / 2\n },\n 0\n )\n .fromTo(\n img.DOM.inner,\n {\n scale: 2.8,\n filter: 'brightness(250%)'\n },\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 destroy() {\n this.destroyed = true;\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n this.container.removeEventListener('mousemove', this.handlePointerMove);\n this.container.removeEventListener('touchmove', this.handlePointerMove);\n this.container.removeEventListener('mousemove', this.initRender);\n this.container.removeEventListener('touchmove', this.initRender);\n this.images.forEach(img => {\n gsap.killTweensOf(img.DOM.el);\n img.destroy();\n });\n }\n\n onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n onImageDeactivated() {\n this.activeImagesCount--;\n if (this.activeImagesCount === 0) this.isIdle = true;\n }\n}\n\nclass ImageTrailVariant3 {\n constructor(container) {\n this.container = container;\n this.rafId = null;\n this.destroyed = false;\n this.DOM = { el: container };\n this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img));\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\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 => {\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 => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n this.cacheMousePos = { ...this.mousePos };\n\n this.rafId = requestAnimationFrame(() => this.render());\n container.removeEventListener('mousemove', initRender);\n container.removeEventListener('touchmove', initRender);\n };\n container.addEventListener('mousemove', initRender);\n container.addEventListener('touchmove', initRender);\n this.handlePointerMove = handlePointerMove;\n this.initRender = initRender;\n }\n\n render() {\n if (this.destroyed) return;\n\n let 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 this.rafId = requestAnimationFrame(() => this.render());\n }\n\n 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 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 / 2,\n y: this.cacheMousePos.y - img.rect.height / 2\n },\n {\n duration: 0.4,\n ease: 'power1',\n scale: 1,\n x: this.mousePos.x - img.rect.width / 2,\n y: this.mousePos.y - img.rect.height / 2\n },\n 0\n )\n .fromTo(\n img.DOM.inner,\n {\n scale: 1.2\n },\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 destroy() {\n this.destroyed = true;\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n this.container.removeEventListener('mousemove', this.handlePointerMove);\n this.container.removeEventListener('touchmove', this.handlePointerMove);\n this.container.removeEventListener('mousemove', this.initRender);\n this.container.removeEventListener('touchmove', this.initRender);\n this.images.forEach(img => {\n gsap.killTweensOf(img.DOM.el);\n img.destroy();\n });\n }\n\n onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n onImageDeactivated() {\n this.activeImagesCount--;\n if (this.activeImagesCount === 0) this.isIdle = true;\n }\n}\n\nclass ImageTrailVariant4 {\n constructor(container) {\n this.container = container;\n this.rafId = null;\n this.destroyed = false;\n this.DOM = { el: container };\n this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img));\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\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 => {\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 => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n this.cacheMousePos = { ...this.mousePos };\n this.rafId = requestAnimationFrame(() => this.render());\n container.removeEventListener('mousemove', initRender);\n container.removeEventListener('touchmove', initRender);\n };\n container.addEventListener('mousemove', initRender);\n container.addEventListener('touchmove', initRender);\n this.handlePointerMove = handlePointerMove;\n this.initRender = initRender;\n }\n\n render() {\n if (this.destroyed) return;\n\n let 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 this.rafId = requestAnimationFrame(() => this.render());\n }\n\n 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 let 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 / 2,\n y: this.cacheMousePos.y - img.rect.height / 2\n },\n {\n duration: 0.4,\n ease: 'power1',\n scale: 1,\n x: this.mousePos.x - img.rect.width / 2,\n y: this.mousePos.y - img.rect.height / 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((400 * distance) / 100, 100)}%)`\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 destroy() {\n this.destroyed = true;\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n this.container.removeEventListener('mousemove', this.handlePointerMove);\n this.container.removeEventListener('touchmove', this.handlePointerMove);\n this.container.removeEventListener('mousemove', this.initRender);\n this.container.removeEventListener('touchmove', this.initRender);\n this.images.forEach(img => {\n gsap.killTweensOf(img.DOM.el);\n img.destroy();\n });\n }\n\n onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n onImageDeactivated() {\n this.activeImagesCount--;\n if (this.activeImagesCount === 0) this.isIdle = true;\n }\n}\n\nclass ImageTrailVariant5 {\n constructor(container) {\n this.container = container;\n this.rafId = null;\n this.destroyed = false;\n this.DOM = { el: container };\n this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img));\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\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 => {\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 => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n this.cacheMousePos = { ...this.mousePos };\n this.rafId = requestAnimationFrame(() => this.render());\n container.removeEventListener('mousemove', initRender);\n container.removeEventListener('touchmove', initRender);\n };\n container.addEventListener('mousemove', initRender);\n container.addEventListener('touchmove', initRender);\n this.handlePointerMove = handlePointerMove;\n this.initRender = initRender;\n }\n\n render() {\n if (this.destroyed) return;\n\n let 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 this.rafId = requestAnimationFrame(() => this.render());\n }\n\n 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 let startAngle = isMovingClockwise ? angle - 10 : angle + 10;\n let 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 / 2,\n y: this.cacheMousePos.y - img.rect.height / 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 / 2 + dx * 70,\n y: this.mousePos.y - img.rect.height / 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 destroy() {\n this.destroyed = true;\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n this.container.removeEventListener('mousemove', this.handlePointerMove);\n this.container.removeEventListener('touchmove', this.handlePointerMove);\n this.container.removeEventListener('mousemove', this.initRender);\n this.container.removeEventListener('touchmove', this.initRender);\n this.images.forEach(img => {\n gsap.killTweensOf(img.DOM.el);\n img.destroy();\n });\n }\n\n onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n onImageDeactivated() {\n this.activeImagesCount--;\n if (this.activeImagesCount === 0) this.isIdle = true;\n }\n}\n\nclass ImageTrailVariant6 {\n constructor(container) {\n this.container = container;\n this.rafId = null;\n this.destroyed = false;\n this.DOM = { el: container };\n this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img));\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\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 => {\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 => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n this.cacheMousePos = { ...this.mousePos };\n this.rafId = requestAnimationFrame(() => this.render());\n container.removeEventListener('mousemove', initRender);\n container.removeEventListener('touchmove', initRender);\n };\n container.addEventListener('mousemove', initRender);\n container.addEventListener('touchmove', initRender);\n this.handlePointerMove = handlePointerMove;\n this.initRender = initRender;\n }\n\n render() {\n if (this.destroyed) return;\n\n let 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 this.rafId = requestAnimationFrame(() => this.render());\n }\n\n mapSpeedToSize(speed, minSize, maxSize) {\n const maxSpeed = 200;\n return minSize + (maxSize - minSize) * Math.min(speed / maxSpeed, 1);\n }\n mapSpeedToBrightness(speed, minB, maxB) {\n const maxSpeed = 70;\n return minB + (maxB - minB) * Math.min(speed / maxSpeed, 1);\n }\n mapSpeedToBlur(speed, minBlur, maxBlur) {\n const maxSpeed = 90;\n return minBlur + (maxBlur - minBlur) * Math.min(speed / maxSpeed, 1);\n }\n mapSpeedToGrayscale(speed, minG, maxG) {\n const maxSpeed = 90;\n return minG + (maxG - minG) * Math.min(speed / maxSpeed, 1);\n }\n\n showNextImage() {\n let dx = this.mousePos.x - this.cacheMousePos.x;\n let dy = this.mousePos.y - this.cacheMousePos.y;\n let 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 let scaleFactor = this.mapSpeedToSize(speed, 0.3, 2);\n let brightnessValue = this.mapSpeedToBrightness(speed, 0, 1.3);\n let blurValue = this.mapSpeedToBlur(speed, 20, 0);\n let 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 / 2,\n y: this.cacheMousePos.y - img.rect.height / 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 / 2,\n y: this.mousePos.y - img.rect.height / 2\n },\n 0\n )\n .fromTo(\n img.DOM.inner,\n {\n scale: 2\n },\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 destroy() {\n this.destroyed = true;\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n this.container.removeEventListener('mousemove', this.handlePointerMove);\n this.container.removeEventListener('touchmove', this.handlePointerMove);\n this.container.removeEventListener('mousemove', this.initRender);\n this.container.removeEventListener('touchmove', this.initRender);\n this.images.forEach(img => {\n gsap.killTweensOf(img.DOM.el);\n img.destroy();\n });\n }\n\n onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n onImageDeactivated() {\n this.activeImagesCount--;\n if (this.activeImagesCount === 0) {\n this.isIdle = true;\n }\n }\n}\n\nfunction getNewPosition(position, offset, arr) {\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}\nclass ImageTrailVariant7 {\n constructor(container) {\n this.container = container;\n this.rafId = null;\n this.destroyed = false;\n this.DOM = { el: container };\n this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img));\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\n this.mousePos = { x: 0, y: 0 };\n this.lastMousePos = { x: 0, y: 0 };\n this.cacheMousePos = { x: 0, y: 0 };\n\n this.visibleImagesCount = 0;\n this.visibleImagesTotal = 9;\n this.visibleImagesTotal = Math.min(this.visibleImagesTotal, this.imagesTotal - 1);\n\n const handlePointerMove = ev => {\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 => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n this.cacheMousePos = { ...this.mousePos };\n this.rafId = requestAnimationFrame(() => this.render());\n container.removeEventListener('mousemove', initRender);\n container.removeEventListener('touchmove', initRender);\n };\n container.addEventListener('mousemove', initRender);\n container.addEventListener('touchmove', initRender);\n this.handlePointerMove = handlePointerMove;\n this.initRender = initRender;\n }\n\n render() {\n if (this.destroyed) return;\n\n let 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 this.rafId = requestAnimationFrame(() => this.render());\n }\n\n 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 / 2,\n y: this.cacheMousePos.y - img.rect.height / 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 / 2,\n y: this.mousePos.y - img.rect.height / 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 destroy() {\n this.destroyed = true;\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n this.container.removeEventListener('mousemove', this.handlePointerMove);\n this.container.removeEventListener('touchmove', this.handlePointerMove);\n this.container.removeEventListener('mousemove', this.initRender);\n this.container.removeEventListener('touchmove', this.initRender);\n this.images.forEach(img => {\n gsap.killTweensOf(img.DOM.el);\n img.destroy();\n });\n }\n\n onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n onImageDeactivated() {\n this.activeImagesCount--;\n }\n}\n\nclass ImageTrailVariant8 {\n constructor(container) {\n this.container = container;\n this.rafId = null;\n this.destroyed = false;\n this.DOM = { el: container };\n this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img));\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\n this.mousePos = { x: 0, y: 0 };\n this.lastMousePos = { x: 0, y: 0 };\n this.cacheMousePos = { x: 0, y: 0 };\n\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 => {\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 => {\n const rect = container.getBoundingClientRect();\n this.mousePos = getLocalPointerPos(ev, rect);\n this.cacheMousePos = { ...this.mousePos };\n this.rafId = requestAnimationFrame(() => this.render());\n container.removeEventListener('mousemove', initRender);\n container.removeEventListener('touchmove', initRender);\n };\n container.addEventListener('mousemove', initRender);\n container.addEventListener('touchmove', initRender);\n this.handlePointerMove = handlePointerMove;\n this.initRender = initRender;\n }\n\n render() {\n if (this.destroyed) return;\n\n let 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 this.rafId = requestAnimationFrame(() => this.render());\n }\n\n 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 / 2,\n y: this.cacheMousePos.y - img.rect.height / 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 / 2,\n y: this.mousePos.y - img.rect.height / 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 destroy() {\n this.destroyed = true;\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n this.container.removeEventListener('mousemove', this.handlePointerMove);\n this.container.removeEventListener('touchmove', this.handlePointerMove);\n this.container.removeEventListener('mousemove', this.initRender);\n this.container.removeEventListener('touchmove', this.initRender);\n this.images.forEach(img => {\n gsap.killTweensOf(img.DOM.el);\n img.destroy();\n });\n }\n\n onImageActivated() {\n this.activeImagesCount++;\n this.isIdle = false;\n }\n onImageDeactivated() {\n this.activeImagesCount--;\n if (this.activeImagesCount === 0) this.isIdle = true;\n }\n}\n\nconst variantMap = {\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\nexport default function ImageTrail({ items = [], variant = 1 }) {\n const containerRef = useRef(null);\n\n useEffect(() => {\n if (!containerRef.current) return;\n\n const Cls = variantMap[variant] || variantMap[1];\n const instance = new Cls(containerRef.current);\n\n return () => {\n instance.destroy();\n };\n }, [variant, items]);\n\n return (\n \n {items.map((url, i) => (\n
\n ))}\n
\n );\n}\n"
}
],
"registryDependencies": [],
diff --git a/public/r/ImageTrail-TS-CSS.json b/public/r/ImageTrail-TS-CSS.json
index b6125e55b..f127a6bae 100644
--- a/public/r/ImageTrail-TS-CSS.json
+++ b/public/r/ImageTrail-TS-CSS.json
@@ -13,7 +13,7 @@
{
"type": "registry:component",
"path": "ImageTrail/ImageTrail.tsx",
- "content": "import { gsap } from 'gsap';\nimport { JSX, useEffect, useRef } from 'react';\nimport './ImageTrail.css';\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 let 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 let 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 = {\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\nexport default function ImageTrail({ items = [], variant = 1 }: ImageTrailProps): JSX.Element {\n const containerRef = useRef(null);\n\n useEffect(() => {\n if (!containerRef.current) return;\n const Cls = variantMap[variant] || variantMap[1];\n new Cls(containerRef.current);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [variant, items]);\n\n return (\n \n {items.map((url, i) => (\n
\n ))}\n
\n );\n}\n"
+ "content": "import { gsap } from 'gsap';\nimport { JSX, useEffect, useRef } from 'react';\nimport './ImageTrail.css';\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 public destroy() {\n window.removeEventListener('resize', this.resize);\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 private rafId: number | null = null;\n private destroyed = false;\n private handlePointerMove!: (ev: MouseEvent | TouchEvent) => void;\n private initRender!: (ev: MouseEvent | TouchEvent) => void;\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 this.rafId = 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 this.handlePointerMove = handlePointerMove;\n this.initRender = initRender;\n }\n\n private render() {\n if (this.destroyed) return;\n\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 this.rafId = 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 public destroy() {\n this.destroyed = true;\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n this.container.removeEventListener('mousemove', this.handlePointerMove as EventListener);\n this.container.removeEventListener('touchmove', this.handlePointerMove as EventListener);\n this.container.removeEventListener('mousemove', this.initRender as EventListener);\n this.container.removeEventListener('touchmove', this.initRender as EventListener);\n this.images.forEach(img => {\n gsap.killTweensOf(img.DOM.el);\n img.destroy();\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 private rafId: number | null = null;\n private destroyed = false;\n private handlePointerMove!: (ev: MouseEvent | TouchEvent) => void;\n private initRender!: (ev: MouseEvent | TouchEvent) => void;\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 this.rafId = 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 this.handlePointerMove = handlePointerMove;\n this.initRender = initRender;\n }\n\n private render() {\n if (this.destroyed) return;\n\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 this.rafId = 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 public destroy() {\n this.destroyed = true;\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n this.container.removeEventListener('mousemove', this.handlePointerMove as EventListener);\n this.container.removeEventListener('touchmove', this.handlePointerMove as EventListener);\n this.container.removeEventListener('mousemove', this.initRender as EventListener);\n this.container.removeEventListener('touchmove', this.initRender as EventListener);\n this.images.forEach(img => {\n gsap.killTweensOf(img.DOM.el);\n img.destroy();\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 private rafId: number | null = null;\n private destroyed = false;\n private handlePointerMove!: (ev: MouseEvent | TouchEvent) => void;\n private initRender!: (ev: MouseEvent | TouchEvent) => void;\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 this.rafId = 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 this.handlePointerMove = handlePointerMove;\n this.initRender = initRender;\n }\n\n private render() {\n if (this.destroyed) return;\n\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 this.rafId = 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 public destroy() {\n this.destroyed = true;\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n this.container.removeEventListener('mousemove', this.handlePointerMove as EventListener);\n this.container.removeEventListener('touchmove', this.handlePointerMove as EventListener);\n this.container.removeEventListener('mousemove', this.initRender as EventListener);\n this.container.removeEventListener('touchmove', this.initRender as EventListener);\n this.images.forEach(img => {\n gsap.killTweensOf(img.DOM.el);\n img.destroy();\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 private rafId: number | null = null;\n private destroyed = false;\n private handlePointerMove!: (ev: MouseEvent | TouchEvent) => void;\n private initRender!: (ev: MouseEvent | TouchEvent) => void;\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 this.rafId = 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 this.handlePointerMove = handlePointerMove;\n this.initRender = initRender;\n }\n\n private render() {\n if (this.destroyed) return;\n\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 this.rafId = 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 let 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 public destroy() {\n this.destroyed = true;\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n this.container.removeEventListener('mousemove', this.handlePointerMove as EventListener);\n this.container.removeEventListener('touchmove', this.handlePointerMove as EventListener);\n this.container.removeEventListener('mousemove', this.initRender as EventListener);\n this.container.removeEventListener('touchmove', this.initRender as EventListener);\n this.images.forEach(img => {\n gsap.killTweensOf(img.DOM.el);\n img.destroy();\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 rafId: number | null = null;\n private destroyed = false;\n private handlePointerMove!: (ev: MouseEvent | TouchEvent) => void;\n private initRender!: (ev: MouseEvent | TouchEvent) => void;\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 this.rafId = 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 this.handlePointerMove = handlePointerMove;\n this.initRender = initRender;\n }\n\n private render() {\n if (this.destroyed) return;\n\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 this.rafId = 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 let 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 public destroy() {\n this.destroyed = true;\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n this.container.removeEventListener('mousemove', this.handlePointerMove as EventListener);\n this.container.removeEventListener('touchmove', this.handlePointerMove as EventListener);\n this.container.removeEventListener('mousemove', this.initRender as EventListener);\n this.container.removeEventListener('touchmove', this.initRender as EventListener);\n this.images.forEach(img => {\n gsap.killTweensOf(img.DOM.el);\n img.destroy();\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 private rafId: number | null = null;\n private destroyed = false;\n private handlePointerMove!: (ev: MouseEvent | TouchEvent) => void;\n private initRender!: (ev: MouseEvent | TouchEvent) => void;\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 this.rafId = 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 this.handlePointerMove = handlePointerMove;\n this.initRender = initRender;\n }\n\n private render() {\n if (this.destroyed) return;\n\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 this.rafId = 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 public destroy() {\n this.destroyed = true;\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n this.container.removeEventListener('mousemove', this.handlePointerMove as EventListener);\n this.container.removeEventListener('touchmove', this.handlePointerMove as EventListener);\n this.container.removeEventListener('mousemove', this.initRender as EventListener);\n this.container.removeEventListener('touchmove', this.initRender as EventListener);\n this.images.forEach(img => {\n gsap.killTweensOf(img.DOM.el);\n img.destroy();\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 rafId: number | null = null;\n private destroyed = false;\n private handlePointerMove!: (ev: MouseEvent | TouchEvent) => void;\n private initRender!: (ev: MouseEvent | TouchEvent) => void;\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 this.rafId = 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 this.handlePointerMove = handlePointerMove;\n this.initRender = initRender;\n }\n\n private render() {\n if (this.destroyed) return;\n\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 this.rafId = 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 public destroy() {\n this.destroyed = true;\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n this.container.removeEventListener('mousemove', this.handlePointerMove as EventListener);\n this.container.removeEventListener('touchmove', this.handlePointerMove as EventListener);\n this.container.removeEventListener('mousemove', this.initRender as EventListener);\n this.container.removeEventListener('touchmove', this.initRender as EventListener);\n this.images.forEach(img => {\n gsap.killTweensOf(img.DOM.el);\n img.destroy();\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 rafId: number | null = null;\n private destroyed = false;\n private handlePointerMove!: (ev: MouseEvent | TouchEvent) => void;\n private initRender!: (ev: MouseEvent | TouchEvent) => void;\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 this.rafId = 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 this.handlePointerMove = handlePointerMove;\n this.initRender = initRender;\n }\n\n private render() {\n if (this.destroyed) return;\n\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 this.rafId = 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 public destroy() {\n this.destroyed = true;\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n this.container.removeEventListener('mousemove', this.handlePointerMove as EventListener);\n this.container.removeEventListener('touchmove', this.handlePointerMove as EventListener);\n this.container.removeEventListener('mousemove', this.initRender as EventListener);\n this.container.removeEventListener('touchmove', this.initRender as EventListener);\n this.images.forEach(img => {\n gsap.killTweensOf(img.DOM.el);\n img.destroy();\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 = {\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\nexport default function ImageTrail({ items = [], variant = 1 }: ImageTrailProps): JSX.Element {\n const containerRef = useRef(null);\n\n useEffect(() => {\n if (!containerRef.current) return;\n const Cls = variantMap[variant] || variantMap[1];\n const instance = new Cls(containerRef.current);\n\n return () => {\n instance.destroy();\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [variant, items]);\n\n return (\n \n {items.map((url, i) => (\n
\n ))}\n
\n );\n}\n"
}
],
"registryDependencies": [],
diff --git a/public/r/ImageTrail-TS-TW.json b/public/r/ImageTrail-TS-TW.json
index fe00260ad..dc9462719 100644
--- a/public/r/ImageTrail-TS-TW.json
+++ b/public/r/ImageTrail-TS-TW.json
@@ -8,7 +8,7 @@
{
"type": "registry:component",
"path": "ImageTrail/ImageTrail.tsx",
- "content": "import { gsap } from 'gsap';\nimport { JSX, useEffect, useRef } from 'react';\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 let 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 let 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 = {\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\nexport default function ImageTrail({ items = [], variant = 1 }: ImageTrailProps): JSX.Element {\n const containerRef = useRef(null);\n\n useEffect(() => {\n if (!containerRef.current) return;\n const Cls = variantMap[variant] || variantMap[1];\n new Cls(containerRef.current);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [variant, items]);\n\n return (\n \n {items.map((url, i) => (\n
\n ))}\n
\n );\n}\n"
+ "content": "import { gsap } from 'gsap';\nimport { JSX, useEffect, useRef } from 'react';\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 public destroy() {\n window.removeEventListener('resize', this.resize);\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 private rafId: number | null = null;\n private destroyed = false;\n private handlePointerMove!: (ev: MouseEvent | TouchEvent) => void;\n private initRender!: (ev: MouseEvent | TouchEvent) => void;\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 this.rafId = 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 this.handlePointerMove = handlePointerMove;\n this.initRender = initRender;\n }\n\n private render() {\n if (this.destroyed) return;\n\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 this.rafId = 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 public destroy() {\n this.destroyed = true;\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n this.container.removeEventListener('mousemove', this.handlePointerMove as EventListener);\n this.container.removeEventListener('touchmove', this.handlePointerMove as EventListener);\n this.container.removeEventListener('mousemove', this.initRender as EventListener);\n this.container.removeEventListener('touchmove', this.initRender as EventListener);\n this.images.forEach(img => {\n gsap.killTweensOf(img.DOM.el);\n img.destroy();\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 private rafId: number | null = null;\n private destroyed = false;\n private handlePointerMove!: (ev: MouseEvent | TouchEvent) => void;\n private initRender!: (ev: MouseEvent | TouchEvent) => void;\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 this.rafId = 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 this.handlePointerMove = handlePointerMove;\n this.initRender = initRender;\n }\n\n private render() {\n if (this.destroyed) return;\n\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 this.rafId = 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 public destroy() {\n this.destroyed = true;\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n this.container.removeEventListener('mousemove', this.handlePointerMove as EventListener);\n this.container.removeEventListener('touchmove', this.handlePointerMove as EventListener);\n this.container.removeEventListener('mousemove', this.initRender as EventListener);\n this.container.removeEventListener('touchmove', this.initRender as EventListener);\n this.images.forEach(img => {\n gsap.killTweensOf(img.DOM.el);\n img.destroy();\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 private rafId: number | null = null;\n private destroyed = false;\n private handlePointerMove!: (ev: MouseEvent | TouchEvent) => void;\n private initRender!: (ev: MouseEvent | TouchEvent) => void;\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 this.rafId = 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 this.handlePointerMove = handlePointerMove;\n this.initRender = initRender;\n }\n\n private render() {\n if (this.destroyed) return;\n\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 this.rafId = 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 public destroy() {\n this.destroyed = true;\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n this.container.removeEventListener('mousemove', this.handlePointerMove as EventListener);\n this.container.removeEventListener('touchmove', this.handlePointerMove as EventListener);\n this.container.removeEventListener('mousemove', this.initRender as EventListener);\n this.container.removeEventListener('touchmove', this.initRender as EventListener);\n this.images.forEach(img => {\n gsap.killTweensOf(img.DOM.el);\n img.destroy();\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 private rafId: number | null = null;\n private destroyed = false;\n private handlePointerMove!: (ev: MouseEvent | TouchEvent) => void;\n private initRender!: (ev: MouseEvent | TouchEvent) => void;\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 this.rafId = 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 this.handlePointerMove = handlePointerMove;\n this.initRender = initRender;\n }\n\n private render() {\n if (this.destroyed) return;\n\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 this.rafId = 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 let 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 public destroy() {\n this.destroyed = true;\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n this.container.removeEventListener('mousemove', this.handlePointerMove as EventListener);\n this.container.removeEventListener('touchmove', this.handlePointerMove as EventListener);\n this.container.removeEventListener('mousemove', this.initRender as EventListener);\n this.container.removeEventListener('touchmove', this.initRender as EventListener);\n this.images.forEach(img => {\n gsap.killTweensOf(img.DOM.el);\n img.destroy();\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 rafId: number | null = null;\n private destroyed = false;\n private handlePointerMove!: (ev: MouseEvent | TouchEvent) => void;\n private initRender!: (ev: MouseEvent | TouchEvent) => void;\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 this.rafId = 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 this.handlePointerMove = handlePointerMove;\n this.initRender = initRender;\n }\n\n private render() {\n if (this.destroyed) return;\n\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 this.rafId = 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 let 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 public destroy() {\n this.destroyed = true;\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n this.container.removeEventListener('mousemove', this.handlePointerMove as EventListener);\n this.container.removeEventListener('touchmove', this.handlePointerMove as EventListener);\n this.container.removeEventListener('mousemove', this.initRender as EventListener);\n this.container.removeEventListener('touchmove', this.initRender as EventListener);\n this.images.forEach(img => {\n gsap.killTweensOf(img.DOM.el);\n img.destroy();\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 private rafId: number | null = null;\n private destroyed = false;\n private handlePointerMove!: (ev: MouseEvent | TouchEvent) => void;\n private initRender!: (ev: MouseEvent | TouchEvent) => void;\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 this.rafId = 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 this.handlePointerMove = handlePointerMove;\n this.initRender = initRender;\n }\n\n private render() {\n if (this.destroyed) return;\n\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 this.rafId = 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 public destroy() {\n this.destroyed = true;\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n this.container.removeEventListener('mousemove', this.handlePointerMove as EventListener);\n this.container.removeEventListener('touchmove', this.handlePointerMove as EventListener);\n this.container.removeEventListener('mousemove', this.initRender as EventListener);\n this.container.removeEventListener('touchmove', this.initRender as EventListener);\n this.images.forEach(img => {\n gsap.killTweensOf(img.DOM.el);\n img.destroy();\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 rafId: number | null = null;\n private destroyed = false;\n private handlePointerMove!: (ev: MouseEvent | TouchEvent) => void;\n private initRender!: (ev: MouseEvent | TouchEvent) => void;\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 this.rafId = 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 this.handlePointerMove = handlePointerMove;\n this.initRender = initRender;\n }\n\n private render() {\n if (this.destroyed) return;\n\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 this.rafId = 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 public destroy() {\n this.destroyed = true;\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n this.container.removeEventListener('mousemove', this.handlePointerMove as EventListener);\n this.container.removeEventListener('touchmove', this.handlePointerMove as EventListener);\n this.container.removeEventListener('mousemove', this.initRender as EventListener);\n this.container.removeEventListener('touchmove', this.initRender as EventListener);\n this.images.forEach(img => {\n gsap.killTweensOf(img.DOM.el);\n img.destroy();\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 rafId: number | null = null;\n private destroyed = false;\n private handlePointerMove!: (ev: MouseEvent | TouchEvent) => void;\n private initRender!: (ev: MouseEvent | TouchEvent) => void;\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 this.rafId = 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 this.handlePointerMove = handlePointerMove;\n this.initRender = initRender;\n }\n\n private render() {\n if (this.destroyed) return;\n\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 this.rafId = 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 public destroy() {\n this.destroyed = true;\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n this.container.removeEventListener('mousemove', this.handlePointerMove as EventListener);\n this.container.removeEventListener('touchmove', this.handlePointerMove as EventListener);\n this.container.removeEventListener('mousemove', this.initRender as EventListener);\n this.container.removeEventListener('touchmove', this.initRender as EventListener);\n this.images.forEach(img => {\n gsap.killTweensOf(img.DOM.el);\n img.destroy();\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 = {\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\nexport default function ImageTrail({ items = [], variant = 1 }: ImageTrailProps): JSX.Element {\n const containerRef = useRef(null);\n\n useEffect(() => {\n if (!containerRef.current) return;\n const Cls = variantMap[variant] || variantMap[1];\n const instance = new Cls(containerRef.current);\n\n return () => {\n instance.destroy();\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [variant, items]);\n\n return (\n \n {items.map((url, i) => (\n
\n ))}\n
\n );\n}\n"
}
],
"registryDependencies": [],
diff --git a/public/r/LineSidebar-JS-CSS.json b/public/r/LineSidebar-JS-CSS.json
index 9d25eb3fb..3600321d8 100644
--- a/public/r/LineSidebar-JS-CSS.json
+++ b/public/r/LineSidebar-JS-CSS.json
@@ -13,7 +13,7 @@
{
"type": "registry:component",
"path": "LineSidebar/LineSidebar.jsx",
- "content": "import { useRef, useState, useCallback, useEffect } from 'react';\nimport './LineSidebar.css';\n\nconst FALLOFF_CURVES = {\n linear: p => p,\n smooth: p => p * p * (3 - 2 * p),\n sharp: p => p * p * p\n};\n\nconst DEFAULT_ITEMS = [\n 'Overview',\n 'Components',\n 'Animations',\n 'Backgrounds',\n 'Showcase',\n 'Playground',\n 'Templates',\n 'Changelog',\n 'Community',\n 'Resources',\n 'Documentation',\n 'Support'\n];\n\nconst LineSidebar = ({\n items = DEFAULT_ITEMS,\n accentColor = '#A855F7',\n textColor = '#c4c4c4',\n markerColor = '#6c6c6c',\n showIndex = true,\n showMarker = true,\n proximityRadius = 100,\n maxShift = 30,\n falloff = 'smooth',\n markerLength = 60,\n markerGap = 0,\n tickScale = 0.5,\n scaleTick = true,\n itemGap = 20,\n fontSize = 1.1,\n smoothing = 100,\n defaultActive = null,\n onItemClick,\n className = ''\n}) => {\n const listRef = useRef(null);\n const itemRefs = useRef([]);\n const targetsRef = useRef([]);\n const currentRef = useRef([]);\n const rafRef = useRef(null);\n const lastRef = useRef(0);\n const activeRef = useRef(defaultActive);\n const smoothingRef = useRef(smoothing);\n const [activeIndex, setActiveIndex] = useState(defaultActive);\n\n activeRef.current = activeIndex;\n smoothingRef.current = smoothing;\n\n // Single rAF loop that eases every item's --effect toward its target using\n // frame-rate independent exponential smoothing, so color, shift and scale\n // all move together without staggering CSS transitions.\n const runFrame = useCallback(now => {\n const dt = Math.min((now - lastRef.current) / 1000, 0.05);\n lastRef.current = now;\n const tau = Math.max(smoothingRef.current, 1) / 1000;\n const k = 1 - Math.exp(-dt / tau);\n\n let moving = false;\n const items = itemRefs.current;\n for (let i = 0; i < items.length; i++) {\n const el = items[i];\n if (!el) continue;\n const target = Math.max(targetsRef.current[i] || 0, activeRef.current === i ? 1 : 0);\n const cur = currentRef.current[i] || 0;\n const next = cur + (target - cur) * k;\n const settled = Math.abs(target - next) < 0.0015;\n const value = settled ? target : next;\n currentRef.current[i] = value;\n el.style.setProperty('--effect', value.toFixed(4));\n if (!settled) moving = true;\n }\n\n rafRef.current = moving ? requestAnimationFrame(runFrame) : null;\n }, []);\n\n const startLoop = useCallback(() => {\n if (rafRef.current != null) return;\n lastRef.current = performance.now();\n rafRef.current = requestAnimationFrame(runFrame);\n }, [runFrame]);\n\n const handlePointerMove = useCallback(\n e => {\n const list = listRef.current;\n if (!list) return;\n const rect = list.getBoundingClientRect();\n const pointerY = e.clientY - rect.top;\n const ease = FALLOFF_CURVES[falloff] ?? FALLOFF_CURVES.linear;\n const items = itemRefs.current;\n for (let i = 0; i < items.length; i++) {\n const el = items[i];\n if (!el) continue;\n const center = el.offsetTop + el.offsetHeight / 2;\n const distance = Math.abs(pointerY - center);\n targetsRef.current[i] = ease(Math.max(0, 1 - distance / proximityRadius));\n }\n startLoop();\n },\n [falloff, proximityRadius, startLoop]\n );\n\n const handlePointerLeave = useCallback(() => {\n targetsRef.current = targetsRef.current.map(() => 0);\n startLoop();\n }, [startLoop]);\n\n const handleClick = useCallback(\n (index, label) => {\n setActiveIndex(index);\n onItemClick?.(index, label);\n },\n [onItemClick]\n );\n\n useEffect(() => {\n startLoop();\n }, [activeIndex, startLoop]);\n\n useEffect(\n () => () => {\n if (rafRef.current != null) cancelAnimationFrame(rafRef.current);\n },\n []\n );\n\n return (\n \n );\n};\n\nexport default LineSidebar;\n"
+ "content": "import { useRef, useState, useCallback, useEffect } from 'react';\nimport './LineSidebar.css';\n\nconst FALLOFF_CURVES = {\n linear: p => p,\n smooth: p => p * p * (3 - 2 * p),\n sharp: p => p * p * p\n};\n\nconst DEFAULT_ITEMS = [\n 'Overview',\n 'Components',\n 'Animations',\n 'Backgrounds',\n 'Showcase',\n 'Playground',\n 'Templates',\n 'Changelog',\n 'Community',\n 'Resources',\n 'Documentation',\n 'Support'\n];\n\nconst LineSidebar = ({\n items = DEFAULT_ITEMS,\n accentColor = '#A855F7',\n textColor = '#c4c4c4',\n markerColor = '#6c6c6c',\n showIndex = true,\n showMarker = true,\n proximityRadius = 100,\n maxShift = 30,\n falloff = 'smooth',\n markerLength = 60,\n markerGap = 0,\n tickScale = 0.5,\n scaleTick = true,\n itemGap = 20,\n fontSize = 1.1,\n smoothing = 100,\n defaultActive = null,\n onItemClick,\n className = ''\n}) => {\n const listRef = useRef(null);\n const itemRefs = useRef([]);\n const targetsRef = useRef([]);\n const currentRef = useRef([]);\n const rafRef = useRef(null);\n const lastRef = useRef(0);\n const activeRef = useRef(defaultActive);\n const smoothingRef = useRef(smoothing);\n const [activeIndex, setActiveIndex] = useState(defaultActive);\n\n activeRef.current = activeIndex;\n smoothingRef.current = smoothing;\n\n // Single rAF loop that eases every item's --effect toward its target using\n // frame-rate independent exponential smoothing, so color, shift and scale\n // all move together without staggering CSS transitions.\n const runFrame = useCallback(now => {\n const dt = Math.min((now - lastRef.current) / 1000, 0.05);\n lastRef.current = now;\n const tau = Math.max(smoothingRef.current, 1) / 1000;\n const k = 1 - Math.exp(-dt / tau);\n\n let moving = false;\n const items = itemRefs.current;\n for (let i = 0; i < items.length; i++) {\n const el = items[i];\n if (!el) continue;\n const target = Math.max(targetsRef.current[i] || 0, activeRef.current === i ? 1 : 0);\n const cur = currentRef.current[i] || 0;\n const next = cur + (target - cur) * k;\n const settled = Math.abs(target - next) < 0.0015;\n const value = settled ? target : next;\n currentRef.current[i] = value;\n el.style.setProperty('--effect', value.toFixed(4));\n if (!settled) moving = true;\n }\n\n rafRef.current = moving ? requestAnimationFrame(runFrame) : null;\n }, []);\n\n const startLoop = useCallback(() => {\n if (rafRef.current != null) {\n cancelAnimationFrame(rafRef.current);\n }\n\n lastRef.current = performance.now();\n rafRef.current = requestAnimationFrame(runFrame);\n }, [runFrame]);\n\n const handlePointerMove = useCallback(\n e => {\n const list = listRef.current;\n if (!list) return;\n const rect = list.getBoundingClientRect();\n const pointerY = e.clientY - rect.top;\n const ease = FALLOFF_CURVES[falloff] ?? FALLOFF_CURVES.linear;\n const items = itemRefs.current;\n for (let i = 0; i < items.length; i++) {\n const el = items[i];\n if (!el) continue;\n const center = el.offsetTop + el.offsetHeight / 2;\n const distance = Math.abs(pointerY - center);\n targetsRef.current[i] = ease(Math.max(0, 1 - distance / proximityRadius));\n }\n startLoop();\n },\n [falloff, proximityRadius, startLoop]\n );\n\n const handlePointerLeave = useCallback(() => {\n targetsRef.current = targetsRef.current.map(() => 0);\n startLoop();\n }, [startLoop]);\n\n const handleClick = useCallback(\n (index, label) => {\n setActiveIndex(index);\n onItemClick?.(index, label);\n },\n [onItemClick]\n );\n\n useEffect(() => {\n startLoop();\n }, [activeIndex, startLoop]);\n\n useEffect(\n () => () => {\n if (rafRef.current != null) cancelAnimationFrame(rafRef.current);\n rafRef.current = null;\n },\n []\n );\n\n return (\n \n );\n};\n\nexport default LineSidebar;\n"
}
],
"registryDependencies": [],
diff --git a/public/r/LineSidebar-JS-TW.json b/public/r/LineSidebar-JS-TW.json
index 37eb99361..bbf7132d6 100644
--- a/public/r/LineSidebar-JS-TW.json
+++ b/public/r/LineSidebar-JS-TW.json
@@ -8,7 +8,7 @@
{
"type": "registry:component",
"path": "LineSidebar/LineSidebar.jsx",
- "content": "import { useRef, useState, useCallback, useEffect } from 'react';\n\nconst FALLOFF_CURVES = {\n linear: p => p,\n smooth: p => p * p * (3 - 2 * p),\n sharp: p => p * p * p\n};\n\nconst DEFAULT_ITEMS = [\n 'Overview',\n 'Components',\n 'Animations',\n 'Backgrounds',\n 'Showcase',\n 'Playground',\n 'Templates',\n 'Changelog',\n 'Community',\n 'Resources',\n 'Documentation',\n 'Support'\n];\n\nconst LineSidebar = ({\n items = DEFAULT_ITEMS,\n accentColor = '#A855F7',\n textColor = '#c4c4c4',\n markerColor = '#6c6c6c',\n showIndex = true,\n showMarker = true,\n proximityRadius = 100,\n maxShift = 30,\n falloff = 'smooth',\n markerLength = 60,\n markerGap = 0,\n tickScale = 0.5,\n scaleTick = true,\n itemGap = 20,\n fontSize = 1.1,\n smoothing = 100,\n defaultActive = null,\n onItemClick,\n className = ''\n}) => {\n const listRef = useRef(null);\n const itemRefs = useRef([]);\n const targetsRef = useRef([]);\n const currentRef = useRef([]);\n const rafRef = useRef(null);\n const lastRef = useRef(0);\n const activeRef = useRef(defaultActive);\n const smoothingRef = useRef(smoothing);\n const [activeIndex, setActiveIndex] = useState(defaultActive);\n\n activeRef.current = activeIndex;\n smoothingRef.current = smoothing;\n\n // Single rAF loop that eases every item's --effect toward its target using\n // frame-rate independent exponential smoothing, so color, shift and scale\n // all move together without staggering CSS transitions.\n const runFrame = useCallback(now => {\n const dt = Math.min((now - lastRef.current) / 1000, 0.05);\n lastRef.current = now;\n const tau = Math.max(smoothingRef.current, 1) / 1000;\n const k = 1 - Math.exp(-dt / tau);\n\n let moving = false;\n const items = itemRefs.current;\n for (let i = 0; i < items.length; i++) {\n const el = items[i];\n if (!el) continue;\n const target = Math.max(targetsRef.current[i] || 0, activeRef.current === i ? 1 : 0);\n const cur = currentRef.current[i] || 0;\n const next = cur + (target - cur) * k;\n const settled = Math.abs(target - next) < 0.0015;\n const value = settled ? target : next;\n currentRef.current[i] = value;\n el.style.setProperty('--effect', value.toFixed(4));\n if (!settled) moving = true;\n }\n\n rafRef.current = moving ? requestAnimationFrame(runFrame) : null;\n }, []);\n\n const startLoop = useCallback(() => {\n if (rafRef.current != null) return;\n lastRef.current = performance.now();\n rafRef.current = requestAnimationFrame(runFrame);\n }, [runFrame]);\n\n const handlePointerMove = useCallback(\n e => {\n const list = listRef.current;\n if (!list) return;\n const rect = list.getBoundingClientRect();\n const pointerY = e.clientY - rect.top;\n const ease = FALLOFF_CURVES[falloff] ?? FALLOFF_CURVES.linear;\n const items = itemRefs.current;\n for (let i = 0; i < items.length; i++) {\n const el = items[i];\n if (!el) continue;\n const center = el.offsetTop + el.offsetHeight / 2;\n const distance = Math.abs(pointerY - center);\n targetsRef.current[i] = ease(Math.max(0, 1 - distance / proximityRadius));\n }\n startLoop();\n },\n [falloff, proximityRadius, startLoop]\n );\n\n const handlePointerLeave = useCallback(() => {\n targetsRef.current = targetsRef.current.map(() => 0);\n startLoop();\n }, [startLoop]);\n\n const handleClick = useCallback(\n (index, label) => {\n setActiveIndex(index);\n onItemClick?.(index, label);\n },\n [onItemClick]\n );\n\n useEffect(() => {\n startLoop();\n }, [activeIndex, startLoop]);\n\n useEffect(\n () => () => {\n if (rafRef.current != null) cancelAnimationFrame(rafRef.current);\n },\n []\n );\n\n const tickClass = showMarker\n ? `after:absolute after:left-[calc(-1*var(--marker-length)-var(--marker-gap))] after:top-[calc(100%+var(--item-gap)/2)] after:h-px after:opacity-50 after:content-[''] last:after:content-none after:[background-color:var(--marker-color)] after:[width:calc(var(--marker-length)*var(--tick-scale))] ${\n scaleTick\n ? \"after:origin-left after:[transform:translateY(-50%)_scaleX(calc(0.7+var(--effect,0)*0.6))]\"\n : 'after:-translate-y-1/2'\n }`\n : '';\n\n return (\n \n );\n};\n\nexport default LineSidebar;\n"
+ "content": "import { useRef, useState, useCallback, useEffect } from 'react';\n\nconst FALLOFF_CURVES = {\n linear: p => p,\n smooth: p => p * p * (3 - 2 * p),\n sharp: p => p * p * p\n};\n\nconst DEFAULT_ITEMS = [\n 'Overview',\n 'Components',\n 'Animations',\n 'Backgrounds',\n 'Showcase',\n 'Playground',\n 'Templates',\n 'Changelog',\n 'Community',\n 'Resources',\n 'Documentation',\n 'Support'\n];\n\nconst LineSidebar = ({\n items = DEFAULT_ITEMS,\n accentColor = '#A855F7',\n textColor = '#c4c4c4',\n markerColor = '#6c6c6c',\n showIndex = true,\n showMarker = true,\n proximityRadius = 100,\n maxShift = 30,\n falloff = 'smooth',\n markerLength = 60,\n markerGap = 0,\n tickScale = 0.5,\n scaleTick = true,\n itemGap = 20,\n fontSize = 1.1,\n smoothing = 100,\n defaultActive = null,\n onItemClick,\n className = ''\n}) => {\n const listRef = useRef(null);\n const itemRefs = useRef([]);\n const targetsRef = useRef([]);\n const currentRef = useRef([]);\n const rafRef = useRef(null);\n const lastRef = useRef(0);\n const activeRef = useRef(defaultActive);\n const smoothingRef = useRef(smoothing);\n const [activeIndex, setActiveIndex] = useState(defaultActive);\n\n activeRef.current = activeIndex;\n smoothingRef.current = smoothing;\n\n // Single rAF loop that eases every item's --effect toward its target using\n // frame-rate independent exponential smoothing, so color, shift and scale\n // all move together without staggering CSS transitions.\n const runFrame = useCallback(now => {\n const dt = Math.min((now - lastRef.current) / 1000, 0.05);\n lastRef.current = now;\n const tau = Math.max(smoothingRef.current, 1) / 1000;\n const k = 1 - Math.exp(-dt / tau);\n\n let moving = false;\n const items = itemRefs.current;\n for (let i = 0; i < items.length; i++) {\n const el = items[i];\n if (!el) continue;\n const target = Math.max(targetsRef.current[i] || 0, activeRef.current === i ? 1 : 0);\n const cur = currentRef.current[i] || 0;\n const next = cur + (target - cur) * k;\n const settled = Math.abs(target - next) < 0.0015;\n const value = settled ? target : next;\n currentRef.current[i] = value;\n el.style.setProperty('--effect', value.toFixed(4));\n if (!settled) moving = true;\n }\n\n rafRef.current = moving ? requestAnimationFrame(runFrame) : null;\n }, []);\n\n const startLoop = useCallback(() => {\n if (rafRef.current != null) {\n cancelAnimationFrame(rafRef.current);\n }\n lastRef.current = performance.now();\n rafRef.current = requestAnimationFrame(runFrame);\n }, [runFrame]);\n\n const handlePointerMove = useCallback(\n e => {\n const list = listRef.current;\n if (!list) return;\n const rect = list.getBoundingClientRect();\n const pointerY = e.clientY - rect.top;\n const ease = FALLOFF_CURVES[falloff] ?? FALLOFF_CURVES.linear;\n const items = itemRefs.current;\n for (let i = 0; i < items.length; i++) {\n const el = items[i];\n if (!el) continue;\n const center = el.offsetTop + el.offsetHeight / 2;\n const distance = Math.abs(pointerY - center);\n targetsRef.current[i] = ease(Math.max(0, 1 - distance / proximityRadius));\n }\n startLoop();\n },\n [falloff, proximityRadius, startLoop]\n );\n\n const handlePointerLeave = useCallback(() => {\n targetsRef.current = targetsRef.current.map(() => 0);\n startLoop();\n }, [startLoop]);\n\n const handleClick = useCallback(\n (index, label) => {\n setActiveIndex(index);\n onItemClick?.(index, label);\n },\n [onItemClick]\n );\n\n useEffect(() => {\n startLoop();\n }, [activeIndex, startLoop]);\n\n useEffect(\n () => () => {\n if (rafRef.current != null) cancelAnimationFrame(rafRef.current);\n rafRef.current = null;\n },\n []\n );\n\n const tickClass = showMarker\n ? `after:absolute after:left-[calc(-1*var(--marker-length)-var(--marker-gap))] after:top-[calc(100%+var(--item-gap)/2)] after:h-px after:opacity-50 after:content-[''] last:after:content-none after:[background-color:var(--marker-color)] after:[width:calc(var(--marker-length)*var(--tick-scale))] ${\n scaleTick\n ? \"after:origin-left after:[transform:translateY(-50%)_scaleX(calc(0.7+var(--effect,0)*0.6))]\"\n : 'after:-translate-y-1/2'\n }`\n : '';\n\n return (\n \n );\n};\n\nexport default LineSidebar;\n"
}
],
"registryDependencies": [],
diff --git a/public/r/LineSidebar-TS-CSS.json b/public/r/LineSidebar-TS-CSS.json
index b341a863d..f43b2478b 100644
--- a/public/r/LineSidebar-TS-CSS.json
+++ b/public/r/LineSidebar-TS-CSS.json
@@ -13,7 +13,7 @@
{
"type": "registry:component",
"path": "LineSidebar/LineSidebar.tsx",
- "content": "import { useRef, useState, useCallback, useEffect, CSSProperties } from 'react';\nimport './LineSidebar.css';\n\ntype Falloff = 'linear' | 'smooth' | 'sharp';\n\nexport interface LineSidebarProps {\n items?: string[];\n accentColor?: string;\n textColor?: string;\n markerColor?: string;\n showIndex?: boolean;\n showMarker?: boolean;\n proximityRadius?: number;\n maxShift?: number;\n falloff?: Falloff;\n markerLength?: number;\n markerGap?: number;\n tickScale?: number;\n scaleTick?: boolean;\n itemGap?: number;\n fontSize?: number;\n smoothing?: number;\n defaultActive?: number | null;\n onItemClick?: (index: number, label: string) => void;\n className?: string;\n}\n\nconst FALLOFF_CURVES: Record number> = {\n linear: p => p,\n smooth: p => p * p * (3 - 2 * p),\n sharp: p => p * p * p\n};\n\nconst DEFAULT_ITEMS = [\n 'Overview',\n 'Components',\n 'Animations',\n 'Backgrounds',\n 'Showcase',\n 'Playground',\n 'Templates',\n 'Changelog',\n 'Community',\n 'Resources',\n 'Documentation',\n 'Support'\n];\n\nconst LineSidebar = ({\n items = DEFAULT_ITEMS,\n accentColor = '#A855F7',\n textColor = '#c4c4c4',\n markerColor = '#6c6c6c',\n showIndex = true,\n showMarker = true,\n proximityRadius = 100,\n maxShift = 30,\n falloff = 'smooth',\n markerLength = 60,\n markerGap = 0,\n tickScale = 0.5,\n scaleTick = true,\n itemGap = 20,\n fontSize = 1.1,\n smoothing = 100,\n defaultActive = null,\n onItemClick,\n className = ''\n}: LineSidebarProps) => {\n const listRef = useRef(null);\n const itemRefs = useRef<(HTMLLIElement | null)[]>([]);\n const targetsRef = useRef([]);\n const currentRef = useRef([]);\n const rafRef = useRef(null);\n const lastRef = useRef(0);\n const activeRef = useRef(defaultActive);\n const smoothingRef = useRef(smoothing);\n const [activeIndex, setActiveIndex] = useState(defaultActive);\n\n activeRef.current = activeIndex;\n smoothingRef.current = smoothing;\n\n // Single rAF loop that eases every item's --effect toward its target using\n // frame-rate independent exponential smoothing, so color, shift and scale\n // all move together without staggering CSS transitions.\n const runFrame = useCallback((now: number) => {\n const dt = Math.min((now - lastRef.current) / 1000, 0.05);\n lastRef.current = now;\n const tau = Math.max(smoothingRef.current, 1) / 1000;\n const k = 1 - Math.exp(-dt / tau);\n\n let moving = false;\n const items = itemRefs.current;\n for (let i = 0; i < items.length; i++) {\n const el = items[i];\n if (!el) continue;\n const target = Math.max(targetsRef.current[i] || 0, activeRef.current === i ? 1 : 0);\n const cur = currentRef.current[i] || 0;\n const next = cur + (target - cur) * k;\n const settled = Math.abs(target - next) < 0.0015;\n const value = settled ? target : next;\n currentRef.current[i] = value;\n el.style.setProperty('--effect', value.toFixed(4));\n if (!settled) moving = true;\n }\n\n rafRef.current = moving ? requestAnimationFrame(runFrame) : null;\n }, []);\n\n const startLoop = useCallback(() => {\n if (rafRef.current != null) return;\n lastRef.current = performance.now();\n rafRef.current = requestAnimationFrame(runFrame);\n }, [runFrame]);\n\n const handlePointerMove = useCallback(\n (e: React.PointerEvent) => {\n const list = listRef.current;\n if (!list) return;\n const rect = list.getBoundingClientRect();\n const pointerY = e.clientY - rect.top;\n const ease = FALLOFF_CURVES[falloff] ?? FALLOFF_CURVES.linear;\n const items = itemRefs.current;\n for (let i = 0; i < items.length; i++) {\n const el = items[i];\n if (!el) continue;\n const center = el.offsetTop + el.offsetHeight / 2;\n const distance = Math.abs(pointerY - center);\n targetsRef.current[i] = ease(Math.max(0, 1 - distance / proximityRadius));\n }\n startLoop();\n },\n [falloff, proximityRadius, startLoop]\n );\n\n const handlePointerLeave = useCallback(() => {\n targetsRef.current = targetsRef.current.map(() => 0);\n startLoop();\n }, [startLoop]);\n\n const handleClick = useCallback(\n (index: number, label: string) => {\n setActiveIndex(index);\n onItemClick?.(index, label);\n },\n [onItemClick]\n );\n\n useEffect(() => {\n startLoop();\n }, [activeIndex, startLoop]);\n\n useEffect(\n () => () => {\n if (rafRef.current != null) cancelAnimationFrame(rafRef.current);\n },\n []\n );\n\n return (\n \n );\n};\n\nexport default LineSidebar;\n"
+ "content": "import { useRef, useState, useCallback, useEffect, CSSProperties } from 'react';\nimport './LineSidebar.css';\n\ntype Falloff = 'linear' | 'smooth' | 'sharp';\n\nexport interface LineSidebarProps {\n items?: string[];\n accentColor?: string;\n textColor?: string;\n markerColor?: string;\n showIndex?: boolean;\n showMarker?: boolean;\n proximityRadius?: number;\n maxShift?: number;\n falloff?: Falloff;\n markerLength?: number;\n markerGap?: number;\n tickScale?: number;\n scaleTick?: boolean;\n itemGap?: number;\n fontSize?: number;\n smoothing?: number;\n defaultActive?: number | null;\n onItemClick?: (index: number, label: string) => void;\n className?: string;\n}\n\nconst FALLOFF_CURVES: Record number> = {\n linear: p => p,\n smooth: p => p * p * (3 - 2 * p),\n sharp: p => p * p * p\n};\n\nconst DEFAULT_ITEMS = [\n 'Overview',\n 'Components',\n 'Animations',\n 'Backgrounds',\n 'Showcase',\n 'Playground',\n 'Templates',\n 'Changelog',\n 'Community',\n 'Resources',\n 'Documentation',\n 'Support'\n];\n\nconst LineSidebar = ({\n items = DEFAULT_ITEMS,\n accentColor = '#A855F7',\n textColor = '#c4c4c4',\n markerColor = '#6c6c6c',\n showIndex = true,\n showMarker = true,\n proximityRadius = 100,\n maxShift = 30,\n falloff = 'smooth',\n markerLength = 60,\n markerGap = 0,\n tickScale = 0.5,\n scaleTick = true,\n itemGap = 20,\n fontSize = 1.1,\n smoothing = 100,\n defaultActive = null,\n onItemClick,\n className = ''\n}: LineSidebarProps) => {\n const listRef = useRef(null);\n const itemRefs = useRef<(HTMLLIElement | null)[]>([]);\n const targetsRef = useRef([]);\n const currentRef = useRef([]);\n const rafRef = useRef(null);\n const lastRef = useRef(0);\n const activeRef = useRef(defaultActive);\n const smoothingRef = useRef(smoothing);\n const [activeIndex, setActiveIndex] = useState(defaultActive);\n\n activeRef.current = activeIndex;\n smoothingRef.current = smoothing;\n\n // Single rAF loop that eases every item's --effect toward its target using\n // frame-rate independent exponential smoothing, so color, shift and scale\n // all move together without staggering CSS transitions.\n const runFrame = useCallback((now: number) => {\n const dt = Math.min((now - lastRef.current) / 1000, 0.05);\n lastRef.current = now;\n const tau = Math.max(smoothingRef.current, 1) / 1000;\n const k = 1 - Math.exp(-dt / tau);\n\n let moving = false;\n const items = itemRefs.current;\n for (let i = 0; i < items.length; i++) {\n const el = items[i];\n if (!el) continue;\n const target = Math.max(targetsRef.current[i] || 0, activeRef.current === i ? 1 : 0);\n const cur = currentRef.current[i] || 0;\n const next = cur + (target - cur) * k;\n const settled = Math.abs(target - next) < 0.0015;\n const value = settled ? target : next;\n currentRef.current[i] = value;\n el.style.setProperty('--effect', value.toFixed(4));\n if (!settled) moving = true;\n }\n\n rafRef.current = moving ? requestAnimationFrame(runFrame) : null;\n }, []);\n\n const startLoop = useCallback(() => {\n if (rafRef.current != null) {\n cancelAnimationFrame(rafRef.current);\n }\n lastRef.current = performance.now();\n rafRef.current = requestAnimationFrame(runFrame);\n }, [runFrame]);\n\n const handlePointerMove = useCallback(\n (e: React.PointerEvent) => {\n const list = listRef.current;\n if (!list) return;\n const rect = list.getBoundingClientRect();\n const pointerY = e.clientY - rect.top;\n const ease = FALLOFF_CURVES[falloff] ?? FALLOFF_CURVES.linear;\n const items = itemRefs.current;\n for (let i = 0; i < items.length; i++) {\n const el = items[i];\n if (!el) continue;\n const center = el.offsetTop + el.offsetHeight / 2;\n const distance = Math.abs(pointerY - center);\n targetsRef.current[i] = ease(Math.max(0, 1 - distance / proximityRadius));\n }\n startLoop();\n },\n [falloff, proximityRadius, startLoop]\n );\n\n const handlePointerLeave = useCallback(() => {\n targetsRef.current = targetsRef.current.map(() => 0);\n startLoop();\n }, [startLoop]);\n\n const handleClick = useCallback(\n (index: number, label: string) => {\n setActiveIndex(index);\n onItemClick?.(index, label);\n },\n [onItemClick]\n );\n\n useEffect(() => {\n startLoop();\n }, [activeIndex, startLoop]);\n\n useEffect(\n () => () => {\n if (rafRef.current != null) cancelAnimationFrame(rafRef.current);\n rafRef.current = null;\n },\n []\n );\n\n return (\n \n );\n};\n\nexport default LineSidebar;\n"
}
],
"registryDependencies": [],
diff --git a/public/r/LineSidebar-TS-TW.json b/public/r/LineSidebar-TS-TW.json
index 6d4e445cc..ad98a2dc6 100644
--- a/public/r/LineSidebar-TS-TW.json
+++ b/public/r/LineSidebar-TS-TW.json
@@ -8,7 +8,7 @@
{
"type": "registry:component",
"path": "LineSidebar/LineSidebar.tsx",
- "content": "import { useRef, useState, useCallback, useEffect, CSSProperties } from 'react';\n\ntype Falloff = 'linear' | 'smooth' | 'sharp';\n\nexport interface LineSidebarProps {\n items?: string[];\n accentColor?: string;\n textColor?: string;\n markerColor?: string;\n showIndex?: boolean;\n showMarker?: boolean;\n proximityRadius?: number;\n maxShift?: number;\n falloff?: Falloff;\n markerLength?: number;\n markerGap?: number;\n tickScale?: number;\n scaleTick?: boolean;\n itemGap?: number;\n fontSize?: number;\n smoothing?: number;\n defaultActive?: number | null;\n onItemClick?: (index: number, label: string) => void;\n className?: string;\n}\n\nconst FALLOFF_CURVES: Record number> = {\n linear: p => p,\n smooth: p => p * p * (3 - 2 * p),\n sharp: p => p * p * p\n};\n\nconst DEFAULT_ITEMS = [\n 'Overview',\n 'Components',\n 'Animations',\n 'Backgrounds',\n 'Showcase',\n 'Playground',\n 'Templates',\n 'Changelog',\n 'Community',\n 'Resources',\n 'Documentation',\n 'Support'\n];\n\nconst LineSidebar = ({\n items = DEFAULT_ITEMS,\n accentColor = '#A855F7',\n textColor = '#c4c4c4',\n markerColor = '#6c6c6c',\n showIndex = true,\n showMarker = true,\n proximityRadius = 100,\n maxShift = 30,\n falloff = 'smooth',\n markerLength = 60,\n markerGap = 0,\n tickScale = 0.5,\n scaleTick = true,\n itemGap = 20,\n fontSize = 1.1,\n smoothing = 100,\n defaultActive = null,\n onItemClick,\n className = ''\n}: LineSidebarProps) => {\n const listRef = useRef(null);\n const itemRefs = useRef<(HTMLLIElement | null)[]>([]);\n const targetsRef = useRef([]);\n const currentRef = useRef([]);\n const rafRef = useRef(null);\n const lastRef = useRef(0);\n const activeRef = useRef(defaultActive);\n const smoothingRef = useRef(smoothing);\n const [activeIndex, setActiveIndex] = useState(defaultActive);\n\n activeRef.current = activeIndex;\n smoothingRef.current = smoothing;\n\n // Single rAF loop that eases every item's --effect toward its target using\n // frame-rate independent exponential smoothing, so color, shift and scale\n // all move together without staggering CSS transitions.\n const runFrame = useCallback((now: number) => {\n const dt = Math.min((now - lastRef.current) / 1000, 0.05);\n lastRef.current = now;\n const tau = Math.max(smoothingRef.current, 1) / 1000;\n const k = 1 - Math.exp(-dt / tau);\n\n let moving = false;\n const items = itemRefs.current;\n for (let i = 0; i < items.length; i++) {\n const el = items[i];\n if (!el) continue;\n const target = Math.max(targetsRef.current[i] || 0, activeRef.current === i ? 1 : 0);\n const cur = currentRef.current[i] || 0;\n const next = cur + (target - cur) * k;\n const settled = Math.abs(target - next) < 0.0015;\n const value = settled ? target : next;\n currentRef.current[i] = value;\n el.style.setProperty('--effect', value.toFixed(4));\n if (!settled) moving = true;\n }\n\n rafRef.current = moving ? requestAnimationFrame(runFrame) : null;\n }, []);\n\n const startLoop = useCallback(() => {\n if (rafRef.current != null) return;\n lastRef.current = performance.now();\n rafRef.current = requestAnimationFrame(runFrame);\n }, [runFrame]);\n\n const handlePointerMove = useCallback(\n (e: React.PointerEvent) => {\n const list = listRef.current;\n if (!list) return;\n const rect = list.getBoundingClientRect();\n const pointerY = e.clientY - rect.top;\n const ease = FALLOFF_CURVES[falloff] ?? FALLOFF_CURVES.linear;\n const items = itemRefs.current;\n for (let i = 0; i < items.length; i++) {\n const el = items[i];\n if (!el) continue;\n const center = el.offsetTop + el.offsetHeight / 2;\n const distance = Math.abs(pointerY - center);\n targetsRef.current[i] = ease(Math.max(0, 1 - distance / proximityRadius));\n }\n startLoop();\n },\n [falloff, proximityRadius, startLoop]\n );\n\n const handlePointerLeave = useCallback(() => {\n targetsRef.current = targetsRef.current.map(() => 0);\n startLoop();\n }, [startLoop]);\n\n const handleClick = useCallback(\n (index: number, label: string) => {\n setActiveIndex(index);\n onItemClick?.(index, label);\n },\n [onItemClick]\n );\n\n useEffect(() => {\n startLoop();\n }, [activeIndex, startLoop]);\n\n useEffect(\n () => () => {\n if (rafRef.current != null) cancelAnimationFrame(rafRef.current);\n },\n []\n );\n\n const tickClass = showMarker\n ? `after:absolute after:left-[calc(-1*var(--marker-length)-var(--marker-gap))] after:top-[calc(100%+var(--item-gap)/2)] after:h-px after:opacity-50 after:content-[''] last:after:content-none after:[background-color:var(--marker-color)] after:[width:calc(var(--marker-length)*var(--tick-scale))] ${\n scaleTick\n ? \"after:origin-left after:[transform:translateY(-50%)_scaleX(calc(0.7+var(--effect,0)*0.6))]\"\n : 'after:-translate-y-1/2'\n }`\n : '';\n\n return (\n \n );\n};\n\nexport default LineSidebar;\n"
+ "content": "import { useRef, useState, useCallback, useEffect, CSSProperties } from 'react';\n\ntype Falloff = 'linear' | 'smooth' | 'sharp';\n\nexport interface LineSidebarProps {\n items?: string[];\n accentColor?: string;\n textColor?: string;\n markerColor?: string;\n showIndex?: boolean;\n showMarker?: boolean;\n proximityRadius?: number;\n maxShift?: number;\n falloff?: Falloff;\n markerLength?: number;\n markerGap?: number;\n tickScale?: number;\n scaleTick?: boolean;\n itemGap?: number;\n fontSize?: number;\n smoothing?: number;\n defaultActive?: number | null;\n onItemClick?: (index: number, label: string) => void;\n className?: string;\n}\n\nconst FALLOFF_CURVES: Record number> = {\n linear: p => p,\n smooth: p => p * p * (3 - 2 * p),\n sharp: p => p * p * p\n};\n\nconst DEFAULT_ITEMS = [\n 'Overview',\n 'Components',\n 'Animations',\n 'Backgrounds',\n 'Showcase',\n 'Playground',\n 'Templates',\n 'Changelog',\n 'Community',\n 'Resources',\n 'Documentation',\n 'Support'\n];\n\nconst LineSidebar = ({\n items = DEFAULT_ITEMS,\n accentColor = '#A855F7',\n textColor = '#c4c4c4',\n markerColor = '#6c6c6c',\n showIndex = true,\n showMarker = true,\n proximityRadius = 100,\n maxShift = 30,\n falloff = 'smooth',\n markerLength = 60,\n markerGap = 0,\n tickScale = 0.5,\n scaleTick = true,\n itemGap = 20,\n fontSize = 1.1,\n smoothing = 100,\n defaultActive = null,\n onItemClick,\n className = ''\n}: LineSidebarProps) => {\n const listRef = useRef(null);\n const itemRefs = useRef<(HTMLLIElement | null)[]>([]);\n const targetsRef = useRef([]);\n const currentRef = useRef([]);\n const rafRef = useRef(null);\n const lastRef = useRef(0);\n const activeRef = useRef(defaultActive);\n const smoothingRef = useRef(smoothing);\n const [activeIndex, setActiveIndex] = useState(defaultActive);\n\n activeRef.current = activeIndex;\n smoothingRef.current = smoothing;\n\n // Single rAF loop that eases every item's --effect toward its target using\n // frame-rate independent exponential smoothing, so color, shift and scale\n // all move together without staggering CSS transitions.\n const runFrame = useCallback((now: number) => {\n const dt = Math.min((now - lastRef.current) / 1000, 0.05);\n lastRef.current = now;\n const tau = Math.max(smoothingRef.current, 1) / 1000;\n const k = 1 - Math.exp(-dt / tau);\n\n let moving = false;\n const items = itemRefs.current;\n for (let i = 0; i < items.length; i++) {\n const el = items[i];\n if (!el) continue;\n const target = Math.max(targetsRef.current[i] || 0, activeRef.current === i ? 1 : 0);\n const cur = currentRef.current[i] || 0;\n const next = cur + (target - cur) * k;\n const settled = Math.abs(target - next) < 0.0015;\n const value = settled ? target : next;\n currentRef.current[i] = value;\n el.style.setProperty('--effect', value.toFixed(4));\n if (!settled) moving = true;\n }\n\n rafRef.current = moving ? requestAnimationFrame(runFrame) : null;\n }, []);\n\n const startLoop = useCallback(() => {\n if (rafRef.current != null) {\n cancelAnimationFrame(rafRef.current);\n }\n lastRef.current = performance.now();\n rafRef.current = requestAnimationFrame(runFrame);\n }, [runFrame]);\n\n const handlePointerMove = useCallback(\n (e: React.PointerEvent) => {\n const list = listRef.current;\n if (!list) return;\n const rect = list.getBoundingClientRect();\n const pointerY = e.clientY - rect.top;\n const ease = FALLOFF_CURVES[falloff] ?? FALLOFF_CURVES.linear;\n const items = itemRefs.current;\n for (let i = 0; i < items.length; i++) {\n const el = items[i];\n if (!el) continue;\n const center = el.offsetTop + el.offsetHeight / 2;\n const distance = Math.abs(pointerY - center);\n targetsRef.current[i] = ease(Math.max(0, 1 - distance / proximityRadius));\n }\n startLoop();\n },\n [falloff, proximityRadius, startLoop]\n );\n\n const handlePointerLeave = useCallback(() => {\n targetsRef.current = targetsRef.current.map(() => 0);\n startLoop();\n }, [startLoop]);\n\n const handleClick = useCallback(\n (index: number, label: string) => {\n setActiveIndex(index);\n onItemClick?.(index, label);\n },\n [onItemClick]\n );\n\n useEffect(() => {\n startLoop();\n }, [activeIndex, startLoop]);\n\n useEffect(\n () => () => {\n if (rafRef.current != null) cancelAnimationFrame(rafRef.current);\n rafRef.current = null;\n },\n []\n );\n\n const tickClass = showMarker\n ? `after:absolute after:left-[calc(-1*var(--marker-length)-var(--marker-gap))] after:top-[calc(100%+var(--item-gap)/2)] after:h-px after:opacity-50 after:content-[''] last:after:content-none after:[background-color:var(--marker-color)] after:[width:calc(var(--marker-length)*var(--tick-scale))] ${\n scaleTick\n ? \"after:origin-left after:[transform:translateY(-50%)_scaleX(calc(0.7+var(--effect,0)*0.6))]\"\n : 'after:-translate-y-1/2'\n }`\n : '';\n\n return (\n \n );\n};\n\nexport default LineSidebar;\n"
}
],
"registryDependencies": [],
diff --git a/public/r/OptionWheel-JS-CSS.json b/public/r/OptionWheel-JS-CSS.json
index 062d5d711..b5ab029e1 100644
--- a/public/r/OptionWheel-JS-CSS.json
+++ b/public/r/OptionWheel-JS-CSS.json
@@ -13,7 +13,7 @@
{
"type": "registry:component",
"path": "OptionWheel/OptionWheel.jsx",
- "content": "import { useRef, useState, useCallback, useEffect } from 'react';\nimport './OptionWheel.css';\n\nconst DEFAULT_ITEMS = [\n 'Ambient',\n 'House',\n 'Techno',\n 'Jazz',\n 'Lo-Fi',\n 'Synthwave',\n 'Trance',\n 'Funk',\n 'Disco',\n 'Hip-Hop',\n 'Chillwave',\n 'Drum & Bass'\n];\n\nconst OptionWheel = ({\n items = DEFAULT_ITEMS,\n defaultSelected = 3,\n onChange,\n textColor = '#a6a6a6',\n activeColor = '#ffffff',\n side = 'left',\n fontSize = 3,\n spacing = 1.4,\n curve = 1,\n tilt = 6,\n blur = 2,\n fade = 0.25,\n minOpacity = 0.05,\n smoothing = 200,\n inset = 80,\n loop = false,\n draggable = true,\n soundUrl = '',\n soundVolume = 0.5,\n className = ''\n}) => {\n const rootRef = useRef(null);\n const itemRefs = useRef([]);\n const posRef = useRef(defaultSelected);\n const targetRef = useRef(defaultSelected);\n const rafRef = useRef(null);\n const lastRef = useRef(0);\n const cfgRef = useRef({});\n const onChangeRef = useRef(onChange);\n const selectedRef = useRef(defaultSelected);\n const wheelTimerRef = useRef(null);\n const dragRef = useRef(null);\n const dragMovedRef = useRef(false);\n const audioRef = useRef(null);\n const audioUrlRef = useRef('');\n const lastTickRef = useRef(0);\n const [selectedIndex, setSelectedIndex] = useState(defaultSelected);\n const [isDragging, setIsDragging] = useState(false);\n\n const remPx = typeof window !== 'undefined' ? parseFloat(getComputedStyle(document.documentElement).fontSize) || 16 : 16;\n\n onChangeRef.current = onChange;\n cfgRef.current = {\n count: items.length,\n items,\n rowH: Math.max(fontSize * spacing * remPx, 1),\n curve,\n tilt,\n blur,\n fade,\n minOpacity,\n side,\n loop,\n smoothing,\n draggable,\n soundUrl,\n soundVolume\n };\n\n // Single rAF loop that eases the wheel position toward its target with\n // frame-rate independent exponential smoothing, then lays every option out\n // along the curve based on its distance from the current position.\n const runFrame = useCallback(now => {\n const dt = Math.min((now - lastRef.current) / 1000, 0.05);\n lastRef.current = now;\n const cfg = cfgRef.current;\n const tau = Math.max(cfg.smoothing, 1) / 1000;\n const k = 1 - Math.exp(-dt / tau);\n\n const target = targetRef.current;\n const cur = posRef.current;\n let next = cur + (target - cur) * k;\n const settled = Math.abs(target - next) < 0.001;\n if (settled) next = target;\n posRef.current = next;\n\n const els = itemRefs.current;\n const n = cfg.count;\n const mirror = cfg.side === 'right' ? -1 : 1;\n // Options sit on a circle whose radius keeps the arc length between two\n // neighbors equal to one row height, so tilt controls how tightly it curls.\n const tiltRad = (cfg.tilt * Math.PI) / 180;\n const R = tiltRad > 0.0005 ? cfg.rowH / tiltRad : 0;\n for (let i = 0; i < n; i++) {\n const el = els[i];\n if (!el) continue;\n let d = i - next;\n if (cfg.loop && n > 1) {\n d = ((d % n) + n) % n;\n if (d > n / 2) d -= n;\n }\n const dist = Math.abs(d);\n let x = 0;\n let y = d * cfg.rowH;\n let rot = 0;\n if (R > 0) {\n const ang = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, d * tiltRad));\n y = R * Math.sin(ang);\n x = -mirror * R * (1 - Math.cos(ang)) * cfg.curve;\n rot = (mirror * ang * 180) / Math.PI;\n }\n el.style.transform = `translate(${x.toFixed(2)}px, calc(${y.toFixed(2)}px - 50%)) rotate(${rot.toFixed(3)}deg)`;\n el.style.opacity = String(Math.max(cfg.minOpacity, 1 - dist * cfg.fade));\n el.style.filter = cfg.blur > 0 ? `blur(${(dist * cfg.blur).toFixed(2)}px)` : 'none';\n el.style.setProperty('--ow-p', Math.max(0, 1 - Math.min(dist, 1)).toFixed(4));\n }\n\n rafRef.current = settled ? null : requestAnimationFrame(runFrame);\n }, []);\n\n const startLoop = useCallback(() => {\n if (rafRef.current != null) return;\n lastRef.current = performance.now();\n rafRef.current = requestAnimationFrame(runFrame);\n }, [runFrame]);\n\n // Optional tick on selection change, throttled so fast scrolling can't spam\n // it, and with playback failures (e.g. autoplay policies) silently ignored.\n const playTick = useCallback(() => {\n const { soundUrl, soundVolume } = cfgRef.current;\n if (!soundUrl) return;\n const now = performance.now();\n if (now - lastTickRef.current < 70) return;\n lastTickRef.current = now;\n if (!audioRef.current || audioUrlRef.current !== soundUrl) {\n audioRef.current = new Audio(soundUrl);\n audioRef.current.preload = 'auto';\n audioUrlRef.current = soundUrl;\n }\n const audio = audioRef.current;\n audio.volume = Math.min(Math.max(soundVolume, 0), 1);\n audio.currentTime = 0;\n audio.play()?.catch(() => {});\n }, []);\n\n const applyTarget = useCallback(\n (value, snap) => {\n const cfg = cfgRef.current;\n let v = value;\n if (!cfg.loop) v = Math.min(Math.max(v, 0), Math.max(cfg.count - 1, 0));\n if (snap) v = Math.round(v);\n targetRef.current = v;\n const idx = ((Math.round(v) % cfg.count) + cfg.count) % cfg.count;\n if (idx !== selectedRef.current) {\n selectedRef.current = idx;\n setSelectedIndex(idx);\n onChangeRef.current?.(idx, cfg.items[idx]);\n playTick();\n }\n startLoop();\n },\n [startLoop, playTick]\n );\n\n // Wheel / touchpad scrolling, registered manually so it can be non-passive.\n useEffect(() => {\n const el = rootRef.current;\n if (!el) return;\n const onWheel = e => {\n e.preventDefault();\n const cfg = cfgRef.current;\n const delta = e.deltaMode === 1 ? e.deltaY * 24 : e.deltaY;\n // Cap each event at one step so notchy mouse wheels move exactly one\n // option per click, while touchpads still scroll continuously.\n const step = Math.max(-1, Math.min(1, delta / cfg.rowH));\n applyTarget(targetRef.current + step, false);\n if (wheelTimerRef.current) clearTimeout(wheelTimerRef.current);\n wheelTimerRef.current = setTimeout(() => applyTarget(targetRef.current, true), 140);\n };\n el.addEventListener('wheel', onWheel, { passive: false });\n return () => {\n el.removeEventListener('wheel', onWheel);\n if (wheelTimerRef.current) clearTimeout(wheelTimerRef.current);\n };\n }, [applyTarget]);\n\n const handlePointerDown = useCallback(e => {\n if (!cfgRef.current.draggable) return;\n dragRef.current = { y: e.clientY, start: targetRef.current, id: e.pointerId };\n dragMovedRef.current = false;\n setIsDragging(true);\n }, []);\n\n const handlePointerMove = useCallback(\n e => {\n const drag = dragRef.current;\n if (!drag) return;\n const dy = e.clientY - drag.y;\n if (!dragMovedRef.current && Math.abs(dy) > 4) {\n dragMovedRef.current = true;\n // Capture only once a real drag starts, so plain clicks still reach\n // the items and navigate to them.\n rootRef.current?.setPointerCapture(drag.id);\n }\n if (dragMovedRef.current) applyTarget(drag.start - dy / cfgRef.current.rowH, false);\n },\n [applyTarget]\n );\n\n const handlePointerEnd = useCallback(() => {\n if (!dragRef.current) return;\n dragRef.current = null;\n setIsDragging(false);\n if (dragMovedRef.current) applyTarget(targetRef.current, true);\n }, [applyTarget]);\n\n const handleItemClick = useCallback(\n index => {\n if (dragMovedRef.current) return;\n const cfg = cfgRef.current;\n const cur = targetRef.current;\n let d = index - (((cur % cfg.count) + cfg.count) % cfg.count);\n if (cfg.loop && cfg.count > 1) {\n if (d > cfg.count / 2) d -= cfg.count;\n else if (d < -cfg.count / 2) d += cfg.count;\n }\n applyTarget(cur + d, true);\n },\n [applyTarget]\n );\n\n const handleKeyDown = useCallback(\n e => {\n let delta = null;\n if (e.key === 'ArrowUp' || e.key === 'ArrowLeft') delta = -1;\n else if (e.key === 'ArrowDown' || e.key === 'ArrowRight') delta = 1;\n if (delta == null) return;\n e.preventDefault();\n applyTarget(Math.round(targetRef.current) + delta, true);\n },\n [applyTarget]\n );\n\n useEffect(() => {\n applyTarget(targetRef.current, false);\n }, [items, fontSize, spacing, curve, tilt, blur, fade, minOpacity, side, loop, smoothing, applyTarget]);\n\n useEffect(\n () => () => {\n if (rafRef.current != null) cancelAnimationFrame(rafRef.current);\n audioRef.current?.pause();\n },\n []\n );\n\n return (\n \n {items.map((label, index) => (\n
{\n itemRefs.current[index] = el;\n }}\n role=\"option\"\n aria-selected={selectedIndex === index}\n className={`option-wheel__item${selectedIndex === index ? ' option-wheel__item--selected' : ''}`}\n onClick={() => handleItemClick(index)}\n >\n {label}\n
\n ))}\n
\n );\n};\n\nexport default OptionWheel;\n"
+ "content": "import { useRef, useState, useCallback, useEffect } from 'react';\nimport './OptionWheel.css';\n\nconst DEFAULT_ITEMS = [\n 'Ambient',\n 'House',\n 'Techno',\n 'Jazz',\n 'Lo-Fi',\n 'Synthwave',\n 'Trance',\n 'Funk',\n 'Disco',\n 'Hip-Hop',\n 'Chillwave',\n 'Drum & Bass'\n];\n\nconst OptionWheel = ({\n items = DEFAULT_ITEMS,\n defaultSelected = 3,\n onChange,\n textColor = '#a6a6a6',\n activeColor = '#ffffff',\n side = 'left',\n fontSize = 3,\n spacing = 1.4,\n curve = 1,\n tilt = 6,\n blur = 2,\n fade = 0.25,\n minOpacity = 0.05,\n smoothing = 200,\n inset = 80,\n loop = false,\n draggable = true,\n soundUrl = '',\n soundVolume = 0.5,\n className = ''\n}) => {\n const rootRef = useRef(null);\n const itemRefs = useRef([]);\n const posRef = useRef(defaultSelected);\n const targetRef = useRef(defaultSelected);\n const rafRef = useRef(null);\n const lastRef = useRef(0);\n const cfgRef = useRef({});\n const onChangeRef = useRef(onChange);\n const selectedRef = useRef(defaultSelected);\n const wheelTimerRef = useRef(null);\n const dragRef = useRef(null);\n const dragMovedRef = useRef(false);\n const audioRef = useRef(null);\n const audioUrlRef = useRef('');\n const lastTickRef = useRef(0);\n const [selectedIndex, setSelectedIndex] = useState(defaultSelected);\n const [isDragging, setIsDragging] = useState(false);\n\n const remPx = typeof window !== 'undefined' ? parseFloat(getComputedStyle(document.documentElement).fontSize) || 16 : 16;\n\n onChangeRef.current = onChange;\n cfgRef.current = {\n count: items.length,\n items,\n rowH: Math.max(fontSize * spacing * remPx, 1),\n curve,\n tilt,\n blur,\n fade,\n minOpacity,\n side,\n loop,\n smoothing,\n draggable,\n soundUrl,\n soundVolume\n };\n\n // Single rAF loop that eases the wheel position toward its target with\n // frame-rate independent exponential smoothing, then lays every option out\n // along the curve based on its distance from the current position.\n const runFrame = useCallback(now => {\n const dt = Math.min((now - lastRef.current) / 1000, 0.05);\n lastRef.current = now;\n const cfg = cfgRef.current;\n const tau = Math.max(cfg.smoothing, 1) / 1000;\n const k = 1 - Math.exp(-dt / tau);\n\n const target = targetRef.current;\n const cur = posRef.current;\n let next = cur + (target - cur) * k;\n const settled = Math.abs(target - next) < 0.001;\n if (settled) next = target;\n posRef.current = next;\n\n const els = itemRefs.current;\n const n = cfg.count;\n const mirror = cfg.side === 'right' ? -1 : 1;\n // Options sit on a circle whose radius keeps the arc length between two\n // neighbors equal to one row height, so tilt controls how tightly it curls.\n const tiltRad = (cfg.tilt * Math.PI) / 180;\n const R = tiltRad > 0.0005 ? cfg.rowH / tiltRad : 0;\n for (let i = 0; i < n; i++) {\n const el = els[i];\n if (!el) continue;\n let d = i - next;\n if (cfg.loop && n > 1) {\n d = ((d % n) + n) % n;\n if (d > n / 2) d -= n;\n }\n const dist = Math.abs(d);\n let x = 0;\n let y = d * cfg.rowH;\n let rot = 0;\n if (R > 0) {\n const ang = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, d * tiltRad));\n y = R * Math.sin(ang);\n x = -mirror * R * (1 - Math.cos(ang)) * cfg.curve;\n rot = (mirror * ang * 180) / Math.PI;\n }\n el.style.transform = `translate(${x.toFixed(2)}px, calc(${y.toFixed(2)}px - 50%)) rotate(${rot.toFixed(3)}deg)`;\n el.style.opacity = String(Math.max(cfg.minOpacity, 1 - dist * cfg.fade));\n el.style.filter = cfg.blur > 0 ? `blur(${(dist * cfg.blur).toFixed(2)}px)` : 'none';\n el.style.setProperty('--ow-p', Math.max(0, 1 - Math.min(dist, 1)).toFixed(4));\n }\n\n rafRef.current = settled ? null : requestAnimationFrame(runFrame);\n }, []);\n\n const startLoop = useCallback(() => {\n if (rafRef.current != null) {\n cancelAnimationFrame(rafRef.current);\n }\n lastRef.current = performance.now();\n rafRef.current = requestAnimationFrame(runFrame);\n }, [runFrame]);\n\n // Optional tick on selection change, throttled so fast scrolling can't spam\n // it, and with playback failures (e.g. autoplay policies) silently ignored.\n const playTick = useCallback(() => {\n const { soundUrl, soundVolume } = cfgRef.current;\n if (!soundUrl) return;\n const now = performance.now();\n if (now - lastTickRef.current < 70) return;\n lastTickRef.current = now;\n if (!audioRef.current || audioUrlRef.current !== soundUrl) {\n audioRef.current = new Audio(soundUrl);\n audioRef.current.preload = 'auto';\n audioUrlRef.current = soundUrl;\n }\n const audio = audioRef.current;\n audio.volume = Math.min(Math.max(soundVolume, 0), 1);\n audio.currentTime = 0;\n audio.play()?.catch(() => {});\n }, []);\n\n const applyTarget = useCallback(\n (value, snap) => {\n const cfg = cfgRef.current;\n let v = value;\n if (!cfg.loop) v = Math.min(Math.max(v, 0), Math.max(cfg.count - 1, 0));\n if (snap) v = Math.round(v);\n targetRef.current = v;\n const idx = ((Math.round(v) % cfg.count) + cfg.count) % cfg.count;\n if (idx !== selectedRef.current) {\n selectedRef.current = idx;\n setSelectedIndex(idx);\n onChangeRef.current?.(idx, cfg.items[idx]);\n playTick();\n }\n startLoop();\n },\n [startLoop, playTick]\n );\n\n // Wheel / touchpad scrolling, registered manually so it can be non-passive.\n useEffect(() => {\n const el = rootRef.current;\n if (!el) return;\n const onWheel = e => {\n e.preventDefault();\n const cfg = cfgRef.current;\n const delta = e.deltaMode === 1 ? e.deltaY * 24 : e.deltaY;\n // Cap each event at one step so notchy mouse wheels move exactly one\n // option per click, while touchpads still scroll continuously.\n const step = Math.max(-1, Math.min(1, delta / cfg.rowH));\n applyTarget(targetRef.current + step, false);\n if (wheelTimerRef.current) clearTimeout(wheelTimerRef.current);\n wheelTimerRef.current = setTimeout(() => applyTarget(targetRef.current, true), 140);\n };\n el.addEventListener('wheel', onWheel, { passive: false });\n return () => {\n el.removeEventListener('wheel', onWheel);\n if (wheelTimerRef.current) clearTimeout(wheelTimerRef.current);\n };\n }, [applyTarget]);\n\n const handlePointerDown = useCallback(e => {\n if (!cfgRef.current.draggable) return;\n dragRef.current = { y: e.clientY, start: targetRef.current, id: e.pointerId };\n dragMovedRef.current = false;\n setIsDragging(true);\n }, []);\n\n const handlePointerMove = useCallback(\n e => {\n const drag = dragRef.current;\n if (!drag) return;\n const dy = e.clientY - drag.y;\n if (!dragMovedRef.current && Math.abs(dy) > 4) {\n dragMovedRef.current = true;\n // Capture only once a real drag starts, so plain clicks still reach\n // the items and navigate to them.\n rootRef.current?.setPointerCapture(drag.id);\n }\n if (dragMovedRef.current) applyTarget(drag.start - dy / cfgRef.current.rowH, false);\n },\n [applyTarget]\n );\n\n const handlePointerEnd = useCallback(() => {\n if (!dragRef.current) return;\n dragRef.current = null;\n setIsDragging(false);\n if (dragMovedRef.current) applyTarget(targetRef.current, true);\n }, [applyTarget]);\n\n const handleItemClick = useCallback(\n index => {\n if (dragMovedRef.current) return;\n const cfg = cfgRef.current;\n const cur = targetRef.current;\n let d = index - (((cur % cfg.count) + cfg.count) % cfg.count);\n if (cfg.loop && cfg.count > 1) {\n if (d > cfg.count / 2) d -= cfg.count;\n else if (d < -cfg.count / 2) d += cfg.count;\n }\n applyTarget(cur + d, true);\n },\n [applyTarget]\n );\n\n const handleKeyDown = useCallback(\n e => {\n let delta = null;\n if (e.key === 'ArrowUp' || e.key === 'ArrowLeft') delta = -1;\n else if (e.key === 'ArrowDown' || e.key === 'ArrowRight') delta = 1;\n if (delta == null) return;\n e.preventDefault();\n applyTarget(Math.round(targetRef.current) + delta, true);\n },\n [applyTarget]\n );\n\n useEffect(() => {\n applyTarget(targetRef.current, false);\n }, [items, fontSize, spacing, curve, tilt, blur, fade, minOpacity, side, loop, smoothing, applyTarget]);\n\n useEffect(\n () => () => {\n if (rafRef.current != null) cancelAnimationFrame(rafRef.current);\n rafRef.current = null;\n audioRef.current?.pause();\n },\n []\n );\n\n return (\n \n {items.map((label, index) => (\n
{\n itemRefs.current[index] = el;\n }}\n role=\"option\"\n aria-selected={selectedIndex === index}\n className={`option-wheel__item${selectedIndex === index ? ' option-wheel__item--selected' : ''}`}\n onClick={() => handleItemClick(index)}\n >\n {label}\n
\n ))}\n
\n );\n};\n\nexport default OptionWheel;\n"
}
],
"registryDependencies": [],
diff --git a/public/r/OptionWheel-JS-TW.json b/public/r/OptionWheel-JS-TW.json
index acbfabbcf..74d329876 100644
--- a/public/r/OptionWheel-JS-TW.json
+++ b/public/r/OptionWheel-JS-TW.json
@@ -8,7 +8,7 @@
{
"type": "registry:component",
"path": "OptionWheel/OptionWheel.jsx",
- "content": "import { useRef, useState, useCallback, useEffect } from 'react';\n\nconst DEFAULT_ITEMS = [\n 'Ambient',\n 'House',\n 'Techno',\n 'Jazz',\n 'Lo-Fi',\n 'Synthwave',\n 'Trance',\n 'Funk',\n 'Disco',\n 'Hip-Hop',\n 'Chillwave',\n 'Drum & Bass'\n];\n\nconst OptionWheel = ({\n items = DEFAULT_ITEMS,\n defaultSelected = 3,\n onChange,\n textColor = '#a6a6a6',\n activeColor = '#ffffff',\n side = 'left',\n fontSize = 3,\n spacing = 1.4,\n curve = 1,\n tilt = 6,\n blur = 2,\n fade = 0.25,\n minOpacity = 0.05,\n smoothing = 200,\n inset = 80,\n loop = false,\n draggable = true,\n soundUrl = '',\n soundVolume = 0.5,\n className = ''\n}) => {\n const rootRef = useRef(null);\n const itemRefs = useRef([]);\n const posRef = useRef(defaultSelected);\n const targetRef = useRef(defaultSelected);\n const rafRef = useRef(null);\n const lastRef = useRef(0);\n const cfgRef = useRef({});\n const onChangeRef = useRef(onChange);\n const selectedRef = useRef(defaultSelected);\n const wheelTimerRef = useRef(null);\n const dragRef = useRef(null);\n const dragMovedRef = useRef(false);\n const audioRef = useRef(null);\n const audioUrlRef = useRef('');\n const lastTickRef = useRef(0);\n const [selectedIndex, setSelectedIndex] = useState(defaultSelected);\n const [isDragging, setIsDragging] = useState(false);\n\n const remPx = typeof window !== 'undefined' ? parseFloat(getComputedStyle(document.documentElement).fontSize) || 16 : 16;\n\n onChangeRef.current = onChange;\n cfgRef.current = {\n count: items.length,\n items,\n rowH: Math.max(fontSize * spacing * remPx, 1),\n curve,\n tilt,\n blur,\n fade,\n minOpacity,\n side,\n loop,\n smoothing,\n draggable,\n soundUrl,\n soundVolume\n };\n\n // Single rAF loop that eases the wheel position toward its target with\n // frame-rate independent exponential smoothing, then lays every option out\n // along the curve based on its distance from the current position.\n const runFrame = useCallback(now => {\n const dt = Math.min((now - lastRef.current) / 1000, 0.05);\n lastRef.current = now;\n const cfg = cfgRef.current;\n const tau = Math.max(cfg.smoothing, 1) / 1000;\n const k = 1 - Math.exp(-dt / tau);\n\n const target = targetRef.current;\n const cur = posRef.current;\n let next = cur + (target - cur) * k;\n const settled = Math.abs(target - next) < 0.001;\n if (settled) next = target;\n posRef.current = next;\n\n const els = itemRefs.current;\n const n = cfg.count;\n const mirror = cfg.side === 'right' ? -1 : 1;\n // Options sit on a circle whose radius keeps the arc length between two\n // neighbors equal to one row height, so tilt controls how tightly it curls.\n const tiltRad = (cfg.tilt * Math.PI) / 180;\n const R = tiltRad > 0.0005 ? cfg.rowH / tiltRad : 0;\n for (let i = 0; i < n; i++) {\n const el = els[i];\n if (!el) continue;\n let d = i - next;\n if (cfg.loop && n > 1) {\n d = ((d % n) + n) % n;\n if (d > n / 2) d -= n;\n }\n const dist = Math.abs(d);\n let x = 0;\n let y = d * cfg.rowH;\n let rot = 0;\n if (R > 0) {\n const ang = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, d * tiltRad));\n y = R * Math.sin(ang);\n x = -mirror * R * (1 - Math.cos(ang)) * cfg.curve;\n rot = (mirror * ang * 180) / Math.PI;\n }\n el.style.transform = `translate(${x.toFixed(2)}px, calc(${y.toFixed(2)}px - 50%)) rotate(${rot.toFixed(3)}deg)`;\n el.style.opacity = String(Math.max(cfg.minOpacity, 1 - dist * cfg.fade));\n el.style.filter = cfg.blur > 0 ? `blur(${(dist * cfg.blur).toFixed(2)}px)` : 'none';\n el.style.setProperty('--ow-p', Math.max(0, 1 - Math.min(dist, 1)).toFixed(4));\n }\n\n rafRef.current = settled ? null : requestAnimationFrame(runFrame);\n }, []);\n\n const startLoop = useCallback(() => {\n if (rafRef.current != null) return;\n lastRef.current = performance.now();\n rafRef.current = requestAnimationFrame(runFrame);\n }, [runFrame]);\n\n // Optional tick on selection change, throttled so fast scrolling can't spam\n // it, and with playback failures (e.g. autoplay policies) silently ignored.\n const playTick = useCallback(() => {\n const { soundUrl, soundVolume } = cfgRef.current;\n if (!soundUrl) return;\n const now = performance.now();\n if (now - lastTickRef.current < 70) return;\n lastTickRef.current = now;\n if (!audioRef.current || audioUrlRef.current !== soundUrl) {\n audioRef.current = new Audio(soundUrl);\n audioRef.current.preload = 'auto';\n audioUrlRef.current = soundUrl;\n }\n const audio = audioRef.current;\n audio.volume = Math.min(Math.max(soundVolume, 0), 1);\n audio.currentTime = 0;\n audio.play()?.catch(() => {});\n }, []);\n\n const applyTarget = useCallback(\n (value, snap) => {\n const cfg = cfgRef.current;\n let v = value;\n if (!cfg.loop) v = Math.min(Math.max(v, 0), Math.max(cfg.count - 1, 0));\n if (snap) v = Math.round(v);\n targetRef.current = v;\n const idx = ((Math.round(v) % cfg.count) + cfg.count) % cfg.count;\n if (idx !== selectedRef.current) {\n selectedRef.current = idx;\n setSelectedIndex(idx);\n onChangeRef.current?.(idx, cfg.items[idx]);\n playTick();\n }\n startLoop();\n },\n [startLoop, playTick]\n );\n\n // Wheel / touchpad scrolling, registered manually so it can be non-passive.\n useEffect(() => {\n const el = rootRef.current;\n if (!el) return;\n const onWheel = e => {\n e.preventDefault();\n const cfg = cfgRef.current;\n const delta = e.deltaMode === 1 ? e.deltaY * 24 : e.deltaY;\n // Cap each event at one step so notchy mouse wheels move exactly one\n // option per click, while touchpads still scroll continuously.\n const step = Math.max(-1, Math.min(1, delta / cfg.rowH));\n applyTarget(targetRef.current + step, false);\n if (wheelTimerRef.current) clearTimeout(wheelTimerRef.current);\n wheelTimerRef.current = setTimeout(() => applyTarget(targetRef.current, true), 140);\n };\n el.addEventListener('wheel', onWheel, { passive: false });\n return () => {\n el.removeEventListener('wheel', onWheel);\n if (wheelTimerRef.current) clearTimeout(wheelTimerRef.current);\n };\n }, [applyTarget]);\n\n const handlePointerDown = useCallback(e => {\n if (!cfgRef.current.draggable) return;\n dragRef.current = { y: e.clientY, start: targetRef.current, id: e.pointerId };\n dragMovedRef.current = false;\n setIsDragging(true);\n }, []);\n\n const handlePointerMove = useCallback(\n e => {\n const drag = dragRef.current;\n if (!drag) return;\n const dy = e.clientY - drag.y;\n if (!dragMovedRef.current && Math.abs(dy) > 4) {\n dragMovedRef.current = true;\n // Capture only once a real drag starts, so plain clicks still reach\n // the items and navigate to them.\n rootRef.current?.setPointerCapture(drag.id);\n }\n if (dragMovedRef.current) applyTarget(drag.start - dy / cfgRef.current.rowH, false);\n },\n [applyTarget]\n );\n\n const handlePointerEnd = useCallback(() => {\n if (!dragRef.current) return;\n dragRef.current = null;\n setIsDragging(false);\n if (dragMovedRef.current) applyTarget(targetRef.current, true);\n }, [applyTarget]);\n\n const handleItemClick = useCallback(\n index => {\n if (dragMovedRef.current) return;\n const cfg = cfgRef.current;\n const cur = targetRef.current;\n let d = index - (((cur % cfg.count) + cfg.count) % cfg.count);\n if (cfg.loop && cfg.count > 1) {\n if (d > cfg.count / 2) d -= cfg.count;\n else if (d < -cfg.count / 2) d += cfg.count;\n }\n applyTarget(cur + d, true);\n },\n [applyTarget]\n );\n\n const handleKeyDown = useCallback(\n e => {\n let delta = null;\n if (e.key === 'ArrowUp' || e.key === 'ArrowLeft') delta = -1;\n else if (e.key === 'ArrowDown' || e.key === 'ArrowRight') delta = 1;\n if (delta == null) return;\n e.preventDefault();\n applyTarget(Math.round(targetRef.current) + delta, true);\n },\n [applyTarget]\n );\n\n useEffect(() => {\n applyTarget(targetRef.current, false);\n }, [items, fontSize, spacing, curve, tilt, blur, fade, minOpacity, side, loop, smoothing, applyTarget]);\n\n useEffect(\n () => () => {\n if (rafRef.current != null) cancelAnimationFrame(rafRef.current);\n audioRef.current?.pause();\n },\n []\n );\n\n return (\n \n {items.map((label, index) => (\n
{\n itemRefs.current[index] = el;\n }}\n role=\"option\"\n aria-selected={selectedIndex === index}\n className={`absolute top-1/2 cursor-pointer whitespace-nowrap leading-none will-change-[transform,opacity,filter] [font-size:var(--ow-font-size)] [color:color-mix(in_srgb,var(--ow-active-color)_calc(var(--ow-p,0)*100%),var(--ow-text-color))] ${\n side === 'right' ? 'right-[var(--ow-inset)] origin-right' : 'left-[var(--ow-inset)] origin-left'\n } ${selectedIndex === index ? 'font-medium' : 'font-extralight'}`}\n onClick={() => handleItemClick(index)}\n >\n {label}\n
\n ))}\n
\n );\n};\n\nexport default OptionWheel;\n"
+ "content": "import { useRef, useState, useCallback, useEffect } from 'react';\n\nconst DEFAULT_ITEMS = [\n 'Ambient',\n 'House',\n 'Techno',\n 'Jazz',\n 'Lo-Fi',\n 'Synthwave',\n 'Trance',\n 'Funk',\n 'Disco',\n 'Hip-Hop',\n 'Chillwave',\n 'Drum & Bass'\n];\n\nconst OptionWheel = ({\n items = DEFAULT_ITEMS,\n defaultSelected = 3,\n onChange,\n textColor = '#a6a6a6',\n activeColor = '#ffffff',\n side = 'left',\n fontSize = 3,\n spacing = 1.4,\n curve = 1,\n tilt = 6,\n blur = 2,\n fade = 0.25,\n minOpacity = 0.05,\n smoothing = 200,\n inset = 80,\n loop = false,\n draggable = true,\n soundUrl = '',\n soundVolume = 0.5,\n className = ''\n}) => {\n const rootRef = useRef(null);\n const itemRefs = useRef([]);\n const posRef = useRef(defaultSelected);\n const targetRef = useRef(defaultSelected);\n const rafRef = useRef(null);\n const lastRef = useRef(0);\n const cfgRef = useRef({});\n const onChangeRef = useRef(onChange);\n const selectedRef = useRef(defaultSelected);\n const wheelTimerRef = useRef(null);\n const dragRef = useRef(null);\n const dragMovedRef = useRef(false);\n const audioRef = useRef(null);\n const audioUrlRef = useRef('');\n const lastTickRef = useRef(0);\n const [selectedIndex, setSelectedIndex] = useState(defaultSelected);\n const [isDragging, setIsDragging] = useState(false);\n\n const remPx = typeof window !== 'undefined' ? parseFloat(getComputedStyle(document.documentElement).fontSize) || 16 : 16;\n\n onChangeRef.current = onChange;\n cfgRef.current = {\n count: items.length,\n items,\n rowH: Math.max(fontSize * spacing * remPx, 1),\n curve,\n tilt,\n blur,\n fade,\n minOpacity,\n side,\n loop,\n smoothing,\n draggable,\n soundUrl,\n soundVolume\n };\n\n // Single rAF loop that eases the wheel position toward its target with\n // frame-rate independent exponential smoothing, then lays every option out\n // along the curve based on its distance from the current position.\n const runFrame = useCallback(now => {\n const dt = Math.min((now - lastRef.current) / 1000, 0.05);\n lastRef.current = now;\n const cfg = cfgRef.current;\n const tau = Math.max(cfg.smoothing, 1) / 1000;\n const k = 1 - Math.exp(-dt / tau);\n\n const target = targetRef.current;\n const cur = posRef.current;\n let next = cur + (target - cur) * k;\n const settled = Math.abs(target - next) < 0.001;\n if (settled) next = target;\n posRef.current = next;\n\n const els = itemRefs.current;\n const n = cfg.count;\n const mirror = cfg.side === 'right' ? -1 : 1;\n // Options sit on a circle whose radius keeps the arc length between two\n // neighbors equal to one row height, so tilt controls how tightly it curls.\n const tiltRad = (cfg.tilt * Math.PI) / 180;\n const R = tiltRad > 0.0005 ? cfg.rowH / tiltRad : 0;\n for (let i = 0; i < n; i++) {\n const el = els[i];\n if (!el) continue;\n let d = i - next;\n if (cfg.loop && n > 1) {\n d = ((d % n) + n) % n;\n if (d > n / 2) d -= n;\n }\n const dist = Math.abs(d);\n let x = 0;\n let y = d * cfg.rowH;\n let rot = 0;\n if (R > 0) {\n const ang = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, d * tiltRad));\n y = R * Math.sin(ang);\n x = -mirror * R * (1 - Math.cos(ang)) * cfg.curve;\n rot = (mirror * ang * 180) / Math.PI;\n }\n el.style.transform = `translate(${x.toFixed(2)}px, calc(${y.toFixed(2)}px - 50%)) rotate(${rot.toFixed(3)}deg)`;\n el.style.opacity = String(Math.max(cfg.minOpacity, 1 - dist * cfg.fade));\n el.style.filter = cfg.blur > 0 ? `blur(${(dist * cfg.blur).toFixed(2)}px)` : 'none';\n el.style.setProperty('--ow-p', Math.max(0, 1 - Math.min(dist, 1)).toFixed(4));\n }\n\n rafRef.current = settled ? null : requestAnimationFrame(runFrame);\n }, []);\n\n const startLoop = useCallback(() => {\n if (rafRef.current != null) {\n cancelAnimationFrame(rafRef.current);\n }\n lastRef.current = performance.now();\n rafRef.current = requestAnimationFrame(runFrame);\n }, [runFrame]);\n\n // Optional tick on selection change, throttled so fast scrolling can't spam\n // it, and with playback failures (e.g. autoplay policies) silently ignored.\n const playTick = useCallback(() => {\n const { soundUrl, soundVolume } = cfgRef.current;\n if (!soundUrl) return;\n const now = performance.now();\n if (now - lastTickRef.current < 70) return;\n lastTickRef.current = now;\n if (!audioRef.current || audioUrlRef.current !== soundUrl) {\n audioRef.current = new Audio(soundUrl);\n audioRef.current.preload = 'auto';\n audioUrlRef.current = soundUrl;\n }\n const audio = audioRef.current;\n audio.volume = Math.min(Math.max(soundVolume, 0), 1);\n audio.currentTime = 0;\n audio.play()?.catch(() => {});\n }, []);\n\n const applyTarget = useCallback(\n (value, snap) => {\n const cfg = cfgRef.current;\n let v = value;\n if (!cfg.loop) v = Math.min(Math.max(v, 0), Math.max(cfg.count - 1, 0));\n if (snap) v = Math.round(v);\n targetRef.current = v;\n const idx = ((Math.round(v) % cfg.count) + cfg.count) % cfg.count;\n if (idx !== selectedRef.current) {\n selectedRef.current = idx;\n setSelectedIndex(idx);\n onChangeRef.current?.(idx, cfg.items[idx]);\n playTick();\n }\n startLoop();\n },\n [startLoop, playTick]\n );\n\n // Wheel / touchpad scrolling, registered manually so it can be non-passive.\n useEffect(() => {\n const el = rootRef.current;\n if (!el) return;\n const onWheel = e => {\n e.preventDefault();\n const cfg = cfgRef.current;\n const delta = e.deltaMode === 1 ? e.deltaY * 24 : e.deltaY;\n // Cap each event at one step so notchy mouse wheels move exactly one\n // option per click, while touchpads still scroll continuously.\n const step = Math.max(-1, Math.min(1, delta / cfg.rowH));\n applyTarget(targetRef.current + step, false);\n if (wheelTimerRef.current) clearTimeout(wheelTimerRef.current);\n wheelTimerRef.current = setTimeout(() => applyTarget(targetRef.current, true), 140);\n };\n el.addEventListener('wheel', onWheel, { passive: false });\n return () => {\n el.removeEventListener('wheel', onWheel);\n if (wheelTimerRef.current) clearTimeout(wheelTimerRef.current);\n };\n }, [applyTarget]);\n\n const handlePointerDown = useCallback(e => {\n if (!cfgRef.current.draggable) return;\n dragRef.current = { y: e.clientY, start: targetRef.current, id: e.pointerId };\n dragMovedRef.current = false;\n setIsDragging(true);\n }, []);\n\n const handlePointerMove = useCallback(\n e => {\n const drag = dragRef.current;\n if (!drag) return;\n const dy = e.clientY - drag.y;\n if (!dragMovedRef.current && Math.abs(dy) > 4) {\n dragMovedRef.current = true;\n // Capture only once a real drag starts, so plain clicks still reach\n // the items and navigate to them.\n rootRef.current?.setPointerCapture(drag.id);\n }\n if (dragMovedRef.current) applyTarget(drag.start - dy / cfgRef.current.rowH, false);\n },\n [applyTarget]\n );\n\n const handlePointerEnd = useCallback(() => {\n if (!dragRef.current) return;\n dragRef.current = null;\n setIsDragging(false);\n if (dragMovedRef.current) applyTarget(targetRef.current, true);\n }, [applyTarget]);\n\n const handleItemClick = useCallback(\n index => {\n if (dragMovedRef.current) return;\n const cfg = cfgRef.current;\n const cur = targetRef.current;\n let d = index - (((cur % cfg.count) + cfg.count) % cfg.count);\n if (cfg.loop && cfg.count > 1) {\n if (d > cfg.count / 2) d -= cfg.count;\n else if (d < -cfg.count / 2) d += cfg.count;\n }\n applyTarget(cur + d, true);\n },\n [applyTarget]\n );\n\n const handleKeyDown = useCallback(\n e => {\n let delta = null;\n if (e.key === 'ArrowUp' || e.key === 'ArrowLeft') delta = -1;\n else if (e.key === 'ArrowDown' || e.key === 'ArrowRight') delta = 1;\n if (delta == null) return;\n e.preventDefault();\n applyTarget(Math.round(targetRef.current) + delta, true);\n },\n [applyTarget]\n );\n\n useEffect(() => {\n applyTarget(targetRef.current, false);\n }, [items, fontSize, spacing, curve, tilt, blur, fade, minOpacity, side, loop, smoothing, applyTarget]);\n\n useEffect(\n () => () => {\n if (rafRef.current != null) cancelAnimationFrame(rafRef.current);\n rafRef.current = null;\n audioRef.current?.pause();\n },\n []\n );\n\n return (\n \n {items.map((label, index) => (\n
{\n itemRefs.current[index] = el;\n }}\n role=\"option\"\n aria-selected={selectedIndex === index}\n className={`absolute top-1/2 cursor-pointer whitespace-nowrap leading-none will-change-[transform,opacity,filter] [font-size:var(--ow-font-size)] [color:color-mix(in_srgb,var(--ow-active-color)_calc(var(--ow-p,0)*100%),var(--ow-text-color))] ${\n side === 'right' ? 'right-[var(--ow-inset)] origin-right' : 'left-[var(--ow-inset)] origin-left'\n } ${selectedIndex === index ? 'font-medium' : 'font-extralight'}`}\n onClick={() => handleItemClick(index)}\n >\n {label}\n
\n ))}\n
\n );\n};\n\nexport default OptionWheel;\n"
}
],
"registryDependencies": [],
diff --git a/public/r/OptionWheel-TS-CSS.json b/public/r/OptionWheel-TS-CSS.json
index d47277677..56904bd34 100644
--- a/public/r/OptionWheel-TS-CSS.json
+++ b/public/r/OptionWheel-TS-CSS.json
@@ -13,7 +13,7 @@
{
"type": "registry:component",
"path": "OptionWheel/OptionWheel.tsx",
- "content": "import { useRef, useState, useCallback, useEffect, CSSProperties } from 'react';\nimport './OptionWheel.css';\n\ntype Side = 'left' | 'right';\n\nexport interface OptionWheelProps {\n items?: string[];\n defaultSelected?: number;\n onChange?: (index: number, item: string) => void;\n textColor?: string;\n activeColor?: string;\n side?: Side;\n fontSize?: number;\n spacing?: number;\n curve?: number;\n tilt?: number;\n blur?: number;\n fade?: number;\n minOpacity?: number;\n smoothing?: number;\n inset?: number;\n loop?: boolean;\n draggable?: boolean;\n soundUrl?: string;\n soundVolume?: number;\n className?: string;\n}\n\ninterface WheelConfig {\n count: number;\n items: string[];\n rowH: number;\n curve: number;\n tilt: number;\n blur: number;\n fade: number;\n minOpacity: number;\n side: Side;\n loop: boolean;\n smoothing: number;\n draggable: boolean;\n soundUrl: string;\n soundVolume: number;\n}\n\nconst DEFAULT_ITEMS = [\n 'Ambient',\n 'House',\n 'Techno',\n 'Jazz',\n 'Lo-Fi',\n 'Synthwave',\n 'Trance',\n 'Funk',\n 'Disco',\n 'Hip-Hop',\n 'Chillwave',\n 'Drum & Bass'\n];\n\nconst OptionWheel = ({\n items = DEFAULT_ITEMS,\n defaultSelected = 3,\n onChange,\n textColor = '#a6a6a6',\n activeColor = '#ffffff',\n side = 'left',\n fontSize = 3,\n spacing = 1.4,\n curve = 1,\n tilt = 6,\n blur = 2,\n fade = 0.25,\n minOpacity = 0.05,\n smoothing = 200,\n inset = 80,\n loop = false,\n draggable = true,\n soundUrl = '',\n soundVolume = 0.5,\n className = ''\n}: OptionWheelProps) => {\n const rootRef = useRef(null);\n const itemRefs = useRef<(HTMLDivElement | null)[]>([]);\n const posRef = useRef(defaultSelected);\n const targetRef = useRef(defaultSelected);\n const rafRef = useRef(null);\n const lastRef = useRef(0);\n const cfgRef = useRef({} as WheelConfig);\n const onChangeRef = useRef(onChange);\n const selectedRef = useRef(defaultSelected);\n const wheelTimerRef = useRef | null>(null);\n const dragRef = useRef<{ y: number; start: number; id: number } | null>(null);\n const dragMovedRef = useRef(false);\n const audioRef = useRef(null);\n const audioUrlRef = useRef('');\n const lastTickRef = useRef(0);\n const [selectedIndex, setSelectedIndex] = useState(defaultSelected);\n const [isDragging, setIsDragging] = useState(false);\n\n const remPx = typeof window !== 'undefined' ? parseFloat(getComputedStyle(document.documentElement).fontSize) || 16 : 16;\n\n onChangeRef.current = onChange;\n cfgRef.current = {\n count: items.length,\n items,\n rowH: Math.max(fontSize * spacing * remPx, 1),\n curve,\n tilt,\n blur,\n fade,\n minOpacity,\n side,\n loop,\n smoothing,\n draggable,\n soundUrl,\n soundVolume\n };\n\n // Single rAF loop that eases the wheel position toward its target with\n // frame-rate independent exponential smoothing, then lays every option out\n // along the curve based on its distance from the current position.\n const runFrame = useCallback((now: number) => {\n const dt = Math.min((now - lastRef.current) / 1000, 0.05);\n lastRef.current = now;\n const cfg = cfgRef.current;\n const tau = Math.max(cfg.smoothing, 1) / 1000;\n const k = 1 - Math.exp(-dt / tau);\n\n const target = targetRef.current;\n const cur = posRef.current;\n let next = cur + (target - cur) * k;\n const settled = Math.abs(target - next) < 0.001;\n if (settled) next = target;\n posRef.current = next;\n\n const els = itemRefs.current;\n const n = cfg.count;\n const mirror = cfg.side === 'right' ? -1 : 1;\n // Options sit on a circle whose radius keeps the arc length between two\n // neighbors equal to one row height, so tilt controls how tightly it curls.\n const tiltRad = (cfg.tilt * Math.PI) / 180;\n const R = tiltRad > 0.0005 ? cfg.rowH / tiltRad : 0;\n for (let i = 0; i < n; i++) {\n const el = els[i];\n if (!el) continue;\n let d = i - next;\n if (cfg.loop && n > 1) {\n d = ((d % n) + n) % n;\n if (d > n / 2) d -= n;\n }\n const dist = Math.abs(d);\n let x = 0;\n let y = d * cfg.rowH;\n let rot = 0;\n if (R > 0) {\n const ang = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, d * tiltRad));\n y = R * Math.sin(ang);\n x = -mirror * R * (1 - Math.cos(ang)) * cfg.curve;\n rot = (mirror * ang * 180) / Math.PI;\n }\n el.style.transform = `translate(${x.toFixed(2)}px, calc(${y.toFixed(2)}px - 50%)) rotate(${rot.toFixed(3)}deg)`;\n el.style.opacity = String(Math.max(cfg.minOpacity, 1 - dist * cfg.fade));\n el.style.filter = cfg.blur > 0 ? `blur(${(dist * cfg.blur).toFixed(2)}px)` : 'none';\n el.style.setProperty('--ow-p', Math.max(0, 1 - Math.min(dist, 1)).toFixed(4));\n }\n\n rafRef.current = settled ? null : requestAnimationFrame(runFrame);\n }, []);\n\n const startLoop = useCallback(() => {\n if (rafRef.current != null) return;\n lastRef.current = performance.now();\n rafRef.current = requestAnimationFrame(runFrame);\n }, [runFrame]);\n\n // Optional tick on selection change, throttled so fast scrolling can't spam\n // it, and with playback failures (e.g. autoplay policies) silently ignored.\n const playTick = useCallback(() => {\n const { soundUrl, soundVolume } = cfgRef.current;\n if (!soundUrl) return;\n const now = performance.now();\n if (now - lastTickRef.current < 70) return;\n lastTickRef.current = now;\n if (!audioRef.current || audioUrlRef.current !== soundUrl) {\n audioRef.current = new Audio(soundUrl);\n audioRef.current.preload = 'auto';\n audioUrlRef.current = soundUrl;\n }\n const audio = audioRef.current;\n audio.volume = Math.min(Math.max(soundVolume, 0), 1);\n audio.currentTime = 0;\n audio.play()?.catch(() => {});\n }, []);\n\n const applyTarget = useCallback(\n (value: number, snap: boolean) => {\n const cfg = cfgRef.current;\n let v = value;\n if (!cfg.loop) v = Math.min(Math.max(v, 0), Math.max(cfg.count - 1, 0));\n if (snap) v = Math.round(v);\n targetRef.current = v;\n const idx = ((Math.round(v) % cfg.count) + cfg.count) % cfg.count;\n if (idx !== selectedRef.current) {\n selectedRef.current = idx;\n setSelectedIndex(idx);\n onChangeRef.current?.(idx, cfg.items[idx]);\n playTick();\n }\n startLoop();\n },\n [startLoop, playTick]\n );\n\n // Wheel / touchpad scrolling, registered manually so it can be non-passive.\n useEffect(() => {\n const el = rootRef.current;\n if (!el) return;\n const onWheel = (e: WheelEvent) => {\n e.preventDefault();\n const cfg = cfgRef.current;\n const delta = e.deltaMode === 1 ? e.deltaY * 24 : e.deltaY;\n // Cap each event at one step so notchy mouse wheels move exactly one\n // option per click, while touchpads still scroll continuously.\n const step = Math.max(-1, Math.min(1, delta / cfg.rowH));\n applyTarget(targetRef.current + step, false);\n if (wheelTimerRef.current) clearTimeout(wheelTimerRef.current);\n wheelTimerRef.current = setTimeout(() => applyTarget(targetRef.current, true), 140);\n };\n el.addEventListener('wheel', onWheel, { passive: false });\n return () => {\n el.removeEventListener('wheel', onWheel);\n if (wheelTimerRef.current) clearTimeout(wheelTimerRef.current);\n };\n }, [applyTarget]);\n\n const handlePointerDown = useCallback((e: React.PointerEvent) => {\n if (!cfgRef.current.draggable) return;\n dragRef.current = { y: e.clientY, start: targetRef.current, id: e.pointerId };\n dragMovedRef.current = false;\n setIsDragging(true);\n }, []);\n\n const handlePointerMove = useCallback(\n (e: React.PointerEvent) => {\n const drag = dragRef.current;\n if (!drag) return;\n const dy = e.clientY - drag.y;\n if (!dragMovedRef.current && Math.abs(dy) > 4) {\n dragMovedRef.current = true;\n // Capture only once a real drag starts, so plain clicks still reach\n // the items and navigate to them.\n rootRef.current?.setPointerCapture(drag.id);\n }\n if (dragMovedRef.current) applyTarget(drag.start - dy / cfgRef.current.rowH, false);\n },\n [applyTarget]\n );\n\n const handlePointerEnd = useCallback(() => {\n if (!dragRef.current) return;\n dragRef.current = null;\n setIsDragging(false);\n if (dragMovedRef.current) applyTarget(targetRef.current, true);\n }, [applyTarget]);\n\n const handleItemClick = useCallback(\n (index: number) => {\n if (dragMovedRef.current) return;\n const cfg = cfgRef.current;\n const cur = targetRef.current;\n let d = index - (((cur % cfg.count) + cfg.count) % cfg.count);\n if (cfg.loop && cfg.count > 1) {\n if (d > cfg.count / 2) d -= cfg.count;\n else if (d < -cfg.count / 2) d += cfg.count;\n }\n applyTarget(cur + d, true);\n },\n [applyTarget]\n );\n\n const handleKeyDown = useCallback(\n (e: React.KeyboardEvent) => {\n let delta: number | null = null;\n if (e.key === 'ArrowUp' || e.key === 'ArrowLeft') delta = -1;\n else if (e.key === 'ArrowDown' || e.key === 'ArrowRight') delta = 1;\n if (delta == null) return;\n e.preventDefault();\n applyTarget(Math.round(targetRef.current) + delta, true);\n },\n [applyTarget]\n );\n\n useEffect(() => {\n applyTarget(targetRef.current, false);\n }, [items, fontSize, spacing, curve, tilt, blur, fade, minOpacity, side, loop, smoothing, applyTarget]);\n\n useEffect(\n () => () => {\n if (rafRef.current != null) cancelAnimationFrame(rafRef.current);\n audioRef.current?.pause();\n },\n []\n );\n\n return (\n \n {items.map((label, index) => (\n
{\n itemRefs.current[index] = el;\n }}\n role=\"option\"\n aria-selected={selectedIndex === index}\n className={`option-wheel__item${selectedIndex === index ? ' option-wheel__item--selected' : ''}`}\n onClick={() => handleItemClick(index)}\n >\n {label}\n
\n ))}\n
\n );\n};\n\nexport default OptionWheel;\n"
+ "content": "import { useRef, useState, useCallback, useEffect, CSSProperties } from 'react';\nimport './OptionWheel.css';\n\ntype Side = 'left' | 'right';\n\nexport interface OptionWheelProps {\n items?: string[];\n defaultSelected?: number;\n onChange?: (index: number, item: string) => void;\n textColor?: string;\n activeColor?: string;\n side?: Side;\n fontSize?: number;\n spacing?: number;\n curve?: number;\n tilt?: number;\n blur?: number;\n fade?: number;\n minOpacity?: number;\n smoothing?: number;\n inset?: number;\n loop?: boolean;\n draggable?: boolean;\n soundUrl?: string;\n soundVolume?: number;\n className?: string;\n}\n\ninterface WheelConfig {\n count: number;\n items: string[];\n rowH: number;\n curve: number;\n tilt: number;\n blur: number;\n fade: number;\n minOpacity: number;\n side: Side;\n loop: boolean;\n smoothing: number;\n draggable: boolean;\n soundUrl: string;\n soundVolume: number;\n}\n\nconst DEFAULT_ITEMS = [\n 'Ambient',\n 'House',\n 'Techno',\n 'Jazz',\n 'Lo-Fi',\n 'Synthwave',\n 'Trance',\n 'Funk',\n 'Disco',\n 'Hip-Hop',\n 'Chillwave',\n 'Drum & Bass'\n];\n\nconst OptionWheel = ({\n items = DEFAULT_ITEMS,\n defaultSelected = 3,\n onChange,\n textColor = '#a6a6a6',\n activeColor = '#ffffff',\n side = 'left',\n fontSize = 3,\n spacing = 1.4,\n curve = 1,\n tilt = 6,\n blur = 2,\n fade = 0.25,\n minOpacity = 0.05,\n smoothing = 200,\n inset = 80,\n loop = false,\n draggable = true,\n soundUrl = '',\n soundVolume = 0.5,\n className = ''\n}: OptionWheelProps) => {\n const rootRef = useRef(null);\n const itemRefs = useRef<(HTMLDivElement | null)[]>([]);\n const posRef = useRef(defaultSelected);\n const targetRef = useRef(defaultSelected);\n const rafRef = useRef(null);\n const lastRef = useRef(0);\n const cfgRef = useRef({} as WheelConfig);\n const onChangeRef = useRef(onChange);\n const selectedRef = useRef(defaultSelected);\n const wheelTimerRef = useRef | null>(null);\n const dragRef = useRef<{ y: number; start: number; id: number } | null>(null);\n const dragMovedRef = useRef(false);\n const audioRef = useRef(null);\n const audioUrlRef = useRef('');\n const lastTickRef = useRef(0);\n const [selectedIndex, setSelectedIndex] = useState(defaultSelected);\n const [isDragging, setIsDragging] = useState(false);\n\n const remPx = typeof window !== 'undefined' ? parseFloat(getComputedStyle(document.documentElement).fontSize) || 16 : 16;\n\n onChangeRef.current = onChange;\n cfgRef.current = {\n count: items.length,\n items,\n rowH: Math.max(fontSize * spacing * remPx, 1),\n curve,\n tilt,\n blur,\n fade,\n minOpacity,\n side,\n loop,\n smoothing,\n draggable,\n soundUrl,\n soundVolume\n };\n\n // Single rAF loop that eases the wheel position toward its target with\n // frame-rate independent exponential smoothing, then lays every option out\n // along the curve based on its distance from the current position.\n const runFrame = useCallback((now: number) => {\n const dt = Math.min((now - lastRef.current) / 1000, 0.05);\n lastRef.current = now;\n const cfg = cfgRef.current;\n const tau = Math.max(cfg.smoothing, 1) / 1000;\n const k = 1 - Math.exp(-dt / tau);\n\n const target = targetRef.current;\n const cur = posRef.current;\n let next = cur + (target - cur) * k;\n const settled = Math.abs(target - next) < 0.001;\n if (settled) next = target;\n posRef.current = next;\n\n const els = itemRefs.current;\n const n = cfg.count;\n const mirror = cfg.side === 'right' ? -1 : 1;\n // Options sit on a circle whose radius keeps the arc length between two\n // neighbors equal to one row height, so tilt controls how tightly it curls.\n const tiltRad = (cfg.tilt * Math.PI) / 180;\n const R = tiltRad > 0.0005 ? cfg.rowH / tiltRad : 0;\n for (let i = 0; i < n; i++) {\n const el = els[i];\n if (!el) continue;\n let d = i - next;\n if (cfg.loop && n > 1) {\n d = ((d % n) + n) % n;\n if (d > n / 2) d -= n;\n }\n const dist = Math.abs(d);\n let x = 0;\n let y = d * cfg.rowH;\n let rot = 0;\n if (R > 0) {\n const ang = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, d * tiltRad));\n y = R * Math.sin(ang);\n x = -mirror * R * (1 - Math.cos(ang)) * cfg.curve;\n rot = (mirror * ang * 180) / Math.PI;\n }\n el.style.transform = `translate(${x.toFixed(2)}px, calc(${y.toFixed(2)}px - 50%)) rotate(${rot.toFixed(3)}deg)`;\n el.style.opacity = String(Math.max(cfg.minOpacity, 1 - dist * cfg.fade));\n el.style.filter = cfg.blur > 0 ? `blur(${(dist * cfg.blur).toFixed(2)}px)` : 'none';\n el.style.setProperty('--ow-p', Math.max(0, 1 - Math.min(dist, 1)).toFixed(4));\n }\n\n rafRef.current = settled ? null : requestAnimationFrame(runFrame);\n }, []);\n\n const startLoop = useCallback(() => {\n if (rafRef.current != null) {\n cancelAnimationFrame(rafRef.current);\n }\n lastRef.current = performance.now();\n rafRef.current = requestAnimationFrame(runFrame);\n }, [runFrame]);\n\n // Optional tick on selection change, throttled so fast scrolling can't spam\n // it, and with playback failures (e.g. autoplay policies) silently ignored.\n const playTick = useCallback(() => {\n const { soundUrl, soundVolume } = cfgRef.current;\n if (!soundUrl) return;\n const now = performance.now();\n if (now - lastTickRef.current < 70) return;\n lastTickRef.current = now;\n if (!audioRef.current || audioUrlRef.current !== soundUrl) {\n audioRef.current = new Audio(soundUrl);\n audioRef.current.preload = 'auto';\n audioUrlRef.current = soundUrl;\n }\n const audio = audioRef.current;\n audio.volume = Math.min(Math.max(soundVolume, 0), 1);\n audio.currentTime = 0;\n audio.play()?.catch(() => {});\n }, []);\n\n const applyTarget = useCallback(\n (value: number, snap: boolean) => {\n const cfg = cfgRef.current;\n let v = value;\n if (!cfg.loop) v = Math.min(Math.max(v, 0), Math.max(cfg.count - 1, 0));\n if (snap) v = Math.round(v);\n targetRef.current = v;\n const idx = ((Math.round(v) % cfg.count) + cfg.count) % cfg.count;\n if (idx !== selectedRef.current) {\n selectedRef.current = idx;\n setSelectedIndex(idx);\n onChangeRef.current?.(idx, cfg.items[idx]);\n playTick();\n }\n startLoop();\n },\n [startLoop, playTick]\n );\n\n // Wheel / touchpad scrolling, registered manually so it can be non-passive.\n useEffect(() => {\n const el = rootRef.current;\n if (!el) return;\n const onWheel = (e: WheelEvent) => {\n e.preventDefault();\n const cfg = cfgRef.current;\n const delta = e.deltaMode === 1 ? e.deltaY * 24 : e.deltaY;\n // Cap each event at one step so notchy mouse wheels move exactly one\n // option per click, while touchpads still scroll continuously.\n const step = Math.max(-1, Math.min(1, delta / cfg.rowH));\n applyTarget(targetRef.current + step, false);\n if (wheelTimerRef.current) clearTimeout(wheelTimerRef.current);\n wheelTimerRef.current = setTimeout(() => applyTarget(targetRef.current, true), 140);\n };\n el.addEventListener('wheel', onWheel, { passive: false });\n return () => {\n el.removeEventListener('wheel', onWheel);\n if (wheelTimerRef.current) clearTimeout(wheelTimerRef.current);\n };\n }, [applyTarget]);\n\n const handlePointerDown = useCallback((e: React.PointerEvent) => {\n if (!cfgRef.current.draggable) return;\n dragRef.current = { y: e.clientY, start: targetRef.current, id: e.pointerId };\n dragMovedRef.current = false;\n setIsDragging(true);\n }, []);\n\n const handlePointerMove = useCallback(\n (e: React.PointerEvent) => {\n const drag = dragRef.current;\n if (!drag) return;\n const dy = e.clientY - drag.y;\n if (!dragMovedRef.current && Math.abs(dy) > 4) {\n dragMovedRef.current = true;\n // Capture only once a real drag starts, so plain clicks still reach\n // the items and navigate to them.\n rootRef.current?.setPointerCapture(drag.id);\n }\n if (dragMovedRef.current) applyTarget(drag.start - dy / cfgRef.current.rowH, false);\n },\n [applyTarget]\n );\n\n const handlePointerEnd = useCallback(() => {\n if (!dragRef.current) return;\n dragRef.current = null;\n setIsDragging(false);\n if (dragMovedRef.current) applyTarget(targetRef.current, true);\n }, [applyTarget]);\n\n const handleItemClick = useCallback(\n (index: number) => {\n if (dragMovedRef.current) return;\n const cfg = cfgRef.current;\n const cur = targetRef.current;\n let d = index - (((cur % cfg.count) + cfg.count) % cfg.count);\n if (cfg.loop && cfg.count > 1) {\n if (d > cfg.count / 2) d -= cfg.count;\n else if (d < -cfg.count / 2) d += cfg.count;\n }\n applyTarget(cur + d, true);\n },\n [applyTarget]\n );\n\n const handleKeyDown = useCallback(\n (e: React.KeyboardEvent) => {\n let delta: number | null = null;\n if (e.key === 'ArrowUp' || e.key === 'ArrowLeft') delta = -1;\n else if (e.key === 'ArrowDown' || e.key === 'ArrowRight') delta = 1;\n if (delta == null) return;\n e.preventDefault();\n applyTarget(Math.round(targetRef.current) + delta, true);\n },\n [applyTarget]\n );\n\n useEffect(() => {\n applyTarget(targetRef.current, false);\n }, [items, fontSize, spacing, curve, tilt, blur, fade, minOpacity, side, loop, smoothing, applyTarget]);\n\n useEffect(\n () => () => {\n if (rafRef.current != null) cancelAnimationFrame(rafRef.current);\n rafRef.current = null;\n audioRef.current?.pause();\n },\n []\n );\n\n return (\n \n {items.map((label, index) => (\n
{\n itemRefs.current[index] = el;\n }}\n role=\"option\"\n aria-selected={selectedIndex === index}\n className={`option-wheel__item${selectedIndex === index ? ' option-wheel__item--selected' : ''}`}\n onClick={() => handleItemClick(index)}\n >\n {label}\n
\n ))}\n
\n );\n};\n\nexport default OptionWheel;\n"
}
],
"registryDependencies": [],
diff --git a/public/r/OptionWheel-TS-TW.json b/public/r/OptionWheel-TS-TW.json
index 385a36e91..ee412653c 100644
--- a/public/r/OptionWheel-TS-TW.json
+++ b/public/r/OptionWheel-TS-TW.json
@@ -8,7 +8,7 @@
{
"type": "registry:component",
"path": "OptionWheel/OptionWheel.tsx",
- "content": "import { useRef, useState, useCallback, useEffect, CSSProperties } from 'react';\n\ntype Side = 'left' | 'right';\n\nexport interface OptionWheelProps {\n items?: string[];\n defaultSelected?: number;\n onChange?: (index: number, item: string) => void;\n textColor?: string;\n activeColor?: string;\n side?: Side;\n fontSize?: number;\n spacing?: number;\n curve?: number;\n tilt?: number;\n blur?: number;\n fade?: number;\n minOpacity?: number;\n smoothing?: number;\n inset?: number;\n loop?: boolean;\n draggable?: boolean;\n soundUrl?: string;\n soundVolume?: number;\n className?: string;\n}\n\ninterface WheelConfig {\n count: number;\n items: string[];\n rowH: number;\n curve: number;\n tilt: number;\n blur: number;\n fade: number;\n minOpacity: number;\n side: Side;\n loop: boolean;\n smoothing: number;\n draggable: boolean;\n soundUrl: string;\n soundVolume: number;\n}\n\nconst DEFAULT_ITEMS = [\n 'Ambient',\n 'House',\n 'Techno',\n 'Jazz',\n 'Lo-Fi',\n 'Synthwave',\n 'Trance',\n 'Funk',\n 'Disco',\n 'Hip-Hop',\n 'Chillwave',\n 'Drum & Bass'\n];\n\nconst OptionWheel = ({\n items = DEFAULT_ITEMS,\n defaultSelected = 3,\n onChange,\n textColor = '#a6a6a6',\n activeColor = '#ffffff',\n side = 'left',\n fontSize = 3,\n spacing = 1.4,\n curve = 1,\n tilt = 6,\n blur = 2,\n fade = 0.25,\n minOpacity = 0.05,\n smoothing = 200,\n inset = 80,\n loop = false,\n draggable = true,\n soundUrl = '',\n soundVolume = 0.5,\n className = ''\n}: OptionWheelProps) => {\n const rootRef = useRef(null);\n const itemRefs = useRef<(HTMLDivElement | null)[]>([]);\n const posRef = useRef(defaultSelected);\n const targetRef = useRef(defaultSelected);\n const rafRef = useRef(null);\n const lastRef = useRef(0);\n const cfgRef = useRef({} as WheelConfig);\n const onChangeRef = useRef(onChange);\n const selectedRef = useRef(defaultSelected);\n const wheelTimerRef = useRef | null>(null);\n const dragRef = useRef<{ y: number; start: number; id: number } | null>(null);\n const dragMovedRef = useRef(false);\n const audioRef = useRef(null);\n const audioUrlRef = useRef('');\n const lastTickRef = useRef(0);\n const [selectedIndex, setSelectedIndex] = useState(defaultSelected);\n const [isDragging, setIsDragging] = useState(false);\n\n const remPx = typeof window !== 'undefined' ? parseFloat(getComputedStyle(document.documentElement).fontSize) || 16 : 16;\n\n onChangeRef.current = onChange;\n cfgRef.current = {\n count: items.length,\n items,\n rowH: Math.max(fontSize * spacing * remPx, 1),\n curve,\n tilt,\n blur,\n fade,\n minOpacity,\n side,\n loop,\n smoothing,\n draggable,\n soundUrl,\n soundVolume\n };\n\n // Single rAF loop that eases the wheel position toward its target with\n // frame-rate independent exponential smoothing, then lays every option out\n // along the curve based on its distance from the current position.\n const runFrame = useCallback((now: number) => {\n const dt = Math.min((now - lastRef.current) / 1000, 0.05);\n lastRef.current = now;\n const cfg = cfgRef.current;\n const tau = Math.max(cfg.smoothing, 1) / 1000;\n const k = 1 - Math.exp(-dt / tau);\n\n const target = targetRef.current;\n const cur = posRef.current;\n let next = cur + (target - cur) * k;\n const settled = Math.abs(target - next) < 0.001;\n if (settled) next = target;\n posRef.current = next;\n\n const els = itemRefs.current;\n const n = cfg.count;\n const mirror = cfg.side === 'right' ? -1 : 1;\n // Options sit on a circle whose radius keeps the arc length between two\n // neighbors equal to one row height, so tilt controls how tightly it curls.\n const tiltRad = (cfg.tilt * Math.PI) / 180;\n const R = tiltRad > 0.0005 ? cfg.rowH / tiltRad : 0;\n for (let i = 0; i < n; i++) {\n const el = els[i];\n if (!el) continue;\n let d = i - next;\n if (cfg.loop && n > 1) {\n d = ((d % n) + n) % n;\n if (d > n / 2) d -= n;\n }\n const dist = Math.abs(d);\n let x = 0;\n let y = d * cfg.rowH;\n let rot = 0;\n if (R > 0) {\n const ang = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, d * tiltRad));\n y = R * Math.sin(ang);\n x = -mirror * R * (1 - Math.cos(ang)) * cfg.curve;\n rot = (mirror * ang * 180) / Math.PI;\n }\n el.style.transform = `translate(${x.toFixed(2)}px, calc(${y.toFixed(2)}px - 50%)) rotate(${rot.toFixed(3)}deg)`;\n el.style.opacity = String(Math.max(cfg.minOpacity, 1 - dist * cfg.fade));\n el.style.filter = cfg.blur > 0 ? `blur(${(dist * cfg.blur).toFixed(2)}px)` : 'none';\n el.style.setProperty('--ow-p', Math.max(0, 1 - Math.min(dist, 1)).toFixed(4));\n }\n\n rafRef.current = settled ? null : requestAnimationFrame(runFrame);\n }, []);\n\n const startLoop = useCallback(() => {\n if (rafRef.current != null) return;\n lastRef.current = performance.now();\n rafRef.current = requestAnimationFrame(runFrame);\n }, [runFrame]);\n\n // Optional tick on selection change, throttled so fast scrolling can't spam\n // it, and with playback failures (e.g. autoplay policies) silently ignored.\n const playTick = useCallback(() => {\n const { soundUrl, soundVolume } = cfgRef.current;\n if (!soundUrl) return;\n const now = performance.now();\n if (now - lastTickRef.current < 70) return;\n lastTickRef.current = now;\n if (!audioRef.current || audioUrlRef.current !== soundUrl) {\n audioRef.current = new Audio(soundUrl);\n audioRef.current.preload = 'auto';\n audioUrlRef.current = soundUrl;\n }\n const audio = audioRef.current;\n audio.volume = Math.min(Math.max(soundVolume, 0), 1);\n audio.currentTime = 0;\n audio.play()?.catch(() => {});\n }, []);\n\n const applyTarget = useCallback(\n (value: number, snap: boolean) => {\n const cfg = cfgRef.current;\n let v = value;\n if (!cfg.loop) v = Math.min(Math.max(v, 0), Math.max(cfg.count - 1, 0));\n if (snap) v = Math.round(v);\n targetRef.current = v;\n const idx = ((Math.round(v) % cfg.count) + cfg.count) % cfg.count;\n if (idx !== selectedRef.current) {\n selectedRef.current = idx;\n setSelectedIndex(idx);\n onChangeRef.current?.(idx, cfg.items[idx]);\n playTick();\n }\n startLoop();\n },\n [startLoop, playTick]\n );\n\n // Wheel / touchpad scrolling, registered manually so it can be non-passive.\n useEffect(() => {\n const el = rootRef.current;\n if (!el) return;\n const onWheel = (e: WheelEvent) => {\n e.preventDefault();\n const cfg = cfgRef.current;\n const delta = e.deltaMode === 1 ? e.deltaY * 24 : e.deltaY;\n // Cap each event at one step so notchy mouse wheels move exactly one\n // option per click, while touchpads still scroll continuously.\n const step = Math.max(-1, Math.min(1, delta / cfg.rowH));\n applyTarget(targetRef.current + step, false);\n if (wheelTimerRef.current) clearTimeout(wheelTimerRef.current);\n wheelTimerRef.current = setTimeout(() => applyTarget(targetRef.current, true), 140);\n };\n el.addEventListener('wheel', onWheel, { passive: false });\n return () => {\n el.removeEventListener('wheel', onWheel);\n if (wheelTimerRef.current) clearTimeout(wheelTimerRef.current);\n };\n }, [applyTarget]);\n\n const handlePointerDown = useCallback((e: React.PointerEvent) => {\n if (!cfgRef.current.draggable) return;\n dragRef.current = { y: e.clientY, start: targetRef.current, id: e.pointerId };\n dragMovedRef.current = false;\n setIsDragging(true);\n }, []);\n\n const handlePointerMove = useCallback(\n (e: React.PointerEvent) => {\n const drag = dragRef.current;\n if (!drag) return;\n const dy = e.clientY - drag.y;\n if (!dragMovedRef.current && Math.abs(dy) > 4) {\n dragMovedRef.current = true;\n // Capture only once a real drag starts, so plain clicks still reach\n // the items and navigate to them.\n rootRef.current?.setPointerCapture(drag.id);\n }\n if (dragMovedRef.current) applyTarget(drag.start - dy / cfgRef.current.rowH, false);\n },\n [applyTarget]\n );\n\n const handlePointerEnd = useCallback(() => {\n if (!dragRef.current) return;\n dragRef.current = null;\n setIsDragging(false);\n if (dragMovedRef.current) applyTarget(targetRef.current, true);\n }, [applyTarget]);\n\n const handleItemClick = useCallback(\n (index: number) => {\n if (dragMovedRef.current) return;\n const cfg = cfgRef.current;\n const cur = targetRef.current;\n let d = index - (((cur % cfg.count) + cfg.count) % cfg.count);\n if (cfg.loop && cfg.count > 1) {\n if (d > cfg.count / 2) d -= cfg.count;\n else if (d < -cfg.count / 2) d += cfg.count;\n }\n applyTarget(cur + d, true);\n },\n [applyTarget]\n );\n\n const handleKeyDown = useCallback(\n (e: React.KeyboardEvent) => {\n let delta: number | null = null;\n if (e.key === 'ArrowUp' || e.key === 'ArrowLeft') delta = -1;\n else if (e.key === 'ArrowDown' || e.key === 'ArrowRight') delta = 1;\n if (delta == null) return;\n e.preventDefault();\n applyTarget(Math.round(targetRef.current) + delta, true);\n },\n [applyTarget]\n );\n\n useEffect(() => {\n applyTarget(targetRef.current, false);\n }, [items, fontSize, spacing, curve, tilt, blur, fade, minOpacity, side, loop, smoothing, applyTarget]);\n\n useEffect(\n () => () => {\n if (rafRef.current != null) cancelAnimationFrame(rafRef.current);\n audioRef.current?.pause();\n },\n []\n );\n\n return (\n \n {items.map((label, index) => (\n
{\n itemRefs.current[index] = el;\n }}\n role=\"option\"\n aria-selected={selectedIndex === index}\n className={`absolute top-1/2 cursor-pointer whitespace-nowrap leading-none will-change-[transform,opacity,filter] [font-size:var(--ow-font-size)] [color:color-mix(in_srgb,var(--ow-active-color)_calc(var(--ow-p,0)*100%),var(--ow-text-color))] ${\n side === 'right' ? 'right-[var(--ow-inset)] origin-right' : 'left-[var(--ow-inset)] origin-left'\n } ${selectedIndex === index ? 'font-medium' : 'font-extralight'}`}\n onClick={() => handleItemClick(index)}\n >\n {label}\n
\n ))}\n
\n );\n};\n\nexport default OptionWheel;\n"
+ "content": "import { useRef, useState, useCallback, useEffect, CSSProperties } from 'react';\n\ntype Side = 'left' | 'right';\n\nexport interface OptionWheelProps {\n items?: string[];\n defaultSelected?: number;\n onChange?: (index: number, item: string) => void;\n textColor?: string;\n activeColor?: string;\n side?: Side;\n fontSize?: number;\n spacing?: number;\n curve?: number;\n tilt?: number;\n blur?: number;\n fade?: number;\n minOpacity?: number;\n smoothing?: number;\n inset?: number;\n loop?: boolean;\n draggable?: boolean;\n soundUrl?: string;\n soundVolume?: number;\n className?: string;\n}\n\ninterface WheelConfig {\n count: number;\n items: string[];\n rowH: number;\n curve: number;\n tilt: number;\n blur: number;\n fade: number;\n minOpacity: number;\n side: Side;\n loop: boolean;\n smoothing: number;\n draggable: boolean;\n soundUrl: string;\n soundVolume: number;\n}\n\nconst DEFAULT_ITEMS = [\n 'Ambient',\n 'House',\n 'Techno',\n 'Jazz',\n 'Lo-Fi',\n 'Synthwave',\n 'Trance',\n 'Funk',\n 'Disco',\n 'Hip-Hop',\n 'Chillwave',\n 'Drum & Bass'\n];\n\nconst OptionWheel = ({\n items = DEFAULT_ITEMS,\n defaultSelected = 3,\n onChange,\n textColor = '#a6a6a6',\n activeColor = '#ffffff',\n side = 'left',\n fontSize = 3,\n spacing = 1.4,\n curve = 1,\n tilt = 6,\n blur = 2,\n fade = 0.25,\n minOpacity = 0.05,\n smoothing = 200,\n inset = 80,\n loop = false,\n draggable = true,\n soundUrl = '',\n soundVolume = 0.5,\n className = ''\n}: OptionWheelProps) => {\n const rootRef = useRef(null);\n const itemRefs = useRef<(HTMLDivElement | null)[]>([]);\n const posRef = useRef(defaultSelected);\n const targetRef = useRef(defaultSelected);\n const rafRef = useRef(null);\n const lastRef = useRef(0);\n const cfgRef = useRef({} as WheelConfig);\n const onChangeRef = useRef(onChange);\n const selectedRef = useRef(defaultSelected);\n const wheelTimerRef = useRef | null>(null);\n const dragRef = useRef<{ y: number; start: number; id: number } | null>(null);\n const dragMovedRef = useRef(false);\n const audioRef = useRef(null);\n const audioUrlRef = useRef('');\n const lastTickRef = useRef(0);\n const [selectedIndex, setSelectedIndex] = useState(defaultSelected);\n const [isDragging, setIsDragging] = useState(false);\n\n const remPx = typeof window !== 'undefined' ? parseFloat(getComputedStyle(document.documentElement).fontSize) || 16 : 16;\n\n onChangeRef.current = onChange;\n cfgRef.current = {\n count: items.length,\n items,\n rowH: Math.max(fontSize * spacing * remPx, 1),\n curve,\n tilt,\n blur,\n fade,\n minOpacity,\n side,\n loop,\n smoothing,\n draggable,\n soundUrl,\n soundVolume\n };\n\n // Single rAF loop that eases the wheel position toward its target with\n // frame-rate independent exponential smoothing, then lays every option out\n // along the curve based on its distance from the current position.\n const runFrame = useCallback((now: number) => {\n const dt = Math.min((now - lastRef.current) / 1000, 0.05);\n lastRef.current = now;\n const cfg = cfgRef.current;\n const tau = Math.max(cfg.smoothing, 1) / 1000;\n const k = 1 - Math.exp(-dt / tau);\n\n const target = targetRef.current;\n const cur = posRef.current;\n let next = cur + (target - cur) * k;\n const settled = Math.abs(target - next) < 0.001;\n if (settled) next = target;\n posRef.current = next;\n\n const els = itemRefs.current;\n const n = cfg.count;\n const mirror = cfg.side === 'right' ? -1 : 1;\n // Options sit on a circle whose radius keeps the arc length between two\n // neighbors equal to one row height, so tilt controls how tightly it curls.\n const tiltRad = (cfg.tilt * Math.PI) / 180;\n const R = tiltRad > 0.0005 ? cfg.rowH / tiltRad : 0;\n for (let i = 0; i < n; i++) {\n const el = els[i];\n if (!el) continue;\n let d = i - next;\n if (cfg.loop && n > 1) {\n d = ((d % n) + n) % n;\n if (d > n / 2) d -= n;\n }\n const dist = Math.abs(d);\n let x = 0;\n let y = d * cfg.rowH;\n let rot = 0;\n if (R > 0) {\n const ang = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, d * tiltRad));\n y = R * Math.sin(ang);\n x = -mirror * R * (1 - Math.cos(ang)) * cfg.curve;\n rot = (mirror * ang * 180) / Math.PI;\n }\n el.style.transform = `translate(${x.toFixed(2)}px, calc(${y.toFixed(2)}px - 50%)) rotate(${rot.toFixed(3)}deg)`;\n el.style.opacity = String(Math.max(cfg.minOpacity, 1 - dist * cfg.fade));\n el.style.filter = cfg.blur > 0 ? `blur(${(dist * cfg.blur).toFixed(2)}px)` : 'none';\n el.style.setProperty('--ow-p', Math.max(0, 1 - Math.min(dist, 1)).toFixed(4));\n }\n\n rafRef.current = settled ? null : requestAnimationFrame(runFrame);\n }, []);\n\n const startLoop = useCallback(() => {\n if (rafRef.current != null) {\n cancelAnimationFrame(rafRef.current);\n }\n lastRef.current = performance.now();\n rafRef.current = requestAnimationFrame(runFrame);\n }, [runFrame]);\n\n // Optional tick on selection change, throttled so fast scrolling can't spam\n // it, and with playback failures (e.g. autoplay policies) silently ignored.\n const playTick = useCallback(() => {\n const { soundUrl, soundVolume } = cfgRef.current;\n if (!soundUrl) return;\n const now = performance.now();\n if (now - lastTickRef.current < 70) return;\n lastTickRef.current = now;\n if (!audioRef.current || audioUrlRef.current !== soundUrl) {\n audioRef.current = new Audio(soundUrl);\n audioRef.current.preload = 'auto';\n audioUrlRef.current = soundUrl;\n }\n const audio = audioRef.current;\n audio.volume = Math.min(Math.max(soundVolume, 0), 1);\n audio.currentTime = 0;\n audio.play()?.catch(() => {});\n }, []);\n\n const applyTarget = useCallback(\n (value: number, snap: boolean) => {\n const cfg = cfgRef.current;\n let v = value;\n if (!cfg.loop) v = Math.min(Math.max(v, 0), Math.max(cfg.count - 1, 0));\n if (snap) v = Math.round(v);\n targetRef.current = v;\n const idx = ((Math.round(v) % cfg.count) + cfg.count) % cfg.count;\n if (idx !== selectedRef.current) {\n selectedRef.current = idx;\n setSelectedIndex(idx);\n onChangeRef.current?.(idx, cfg.items[idx]);\n playTick();\n }\n startLoop();\n },\n [startLoop, playTick]\n );\n\n // Wheel / touchpad scrolling, registered manually so it can be non-passive.\n useEffect(() => {\n const el = rootRef.current;\n if (!el) return;\n const onWheel = (e: WheelEvent) => {\n e.preventDefault();\n const cfg = cfgRef.current;\n const delta = e.deltaMode === 1 ? e.deltaY * 24 : e.deltaY;\n // Cap each event at one step so notchy mouse wheels move exactly one\n // option per click, while touchpads still scroll continuously.\n const step = Math.max(-1, Math.min(1, delta / cfg.rowH));\n applyTarget(targetRef.current + step, false);\n if (wheelTimerRef.current) clearTimeout(wheelTimerRef.current);\n wheelTimerRef.current = setTimeout(() => applyTarget(targetRef.current, true), 140);\n };\n el.addEventListener('wheel', onWheel, { passive: false });\n return () => {\n el.removeEventListener('wheel', onWheel);\n if (wheelTimerRef.current) clearTimeout(wheelTimerRef.current);\n };\n }, [applyTarget]);\n\n const handlePointerDown = useCallback((e: React.PointerEvent) => {\n if (!cfgRef.current.draggable) return;\n dragRef.current = { y: e.clientY, start: targetRef.current, id: e.pointerId };\n dragMovedRef.current = false;\n setIsDragging(true);\n }, []);\n\n const handlePointerMove = useCallback(\n (e: React.PointerEvent) => {\n const drag = dragRef.current;\n if (!drag) return;\n const dy = e.clientY - drag.y;\n if (!dragMovedRef.current && Math.abs(dy) > 4) {\n dragMovedRef.current = true;\n // Capture only once a real drag starts, so plain clicks still reach\n // the items and navigate to them.\n rootRef.current?.setPointerCapture(drag.id);\n }\n if (dragMovedRef.current) applyTarget(drag.start - dy / cfgRef.current.rowH, false);\n },\n [applyTarget]\n );\n\n const handlePointerEnd = useCallback(() => {\n if (!dragRef.current) return;\n dragRef.current = null;\n setIsDragging(false);\n if (dragMovedRef.current) applyTarget(targetRef.current, true);\n }, [applyTarget]);\n\n const handleItemClick = useCallback(\n (index: number) => {\n if (dragMovedRef.current) return;\n const cfg = cfgRef.current;\n const cur = targetRef.current;\n let d = index - (((cur % cfg.count) + cfg.count) % cfg.count);\n if (cfg.loop && cfg.count > 1) {\n if (d > cfg.count / 2) d -= cfg.count;\n else if (d < -cfg.count / 2) d += cfg.count;\n }\n applyTarget(cur + d, true);\n },\n [applyTarget]\n );\n\n const handleKeyDown = useCallback(\n (e: React.KeyboardEvent) => {\n let delta: number | null = null;\n if (e.key === 'ArrowUp' || e.key === 'ArrowLeft') delta = -1;\n else if (e.key === 'ArrowDown' || e.key === 'ArrowRight') delta = 1;\n if (delta == null) return;\n e.preventDefault();\n applyTarget(Math.round(targetRef.current) + delta, true);\n },\n [applyTarget]\n );\n\n useEffect(() => {\n applyTarget(targetRef.current, false);\n }, [items, fontSize, spacing, curve, tilt, blur, fade, minOpacity, side, loop, smoothing, applyTarget]);\n\n useEffect(\n () => () => {\n if (rafRef.current != null) cancelAnimationFrame(rafRef.current);\n rafRef.current = null;\n audioRef.current?.pause();\n },\n []\n );\n\n return (\n \n {items.map((label, index) => (\n
{\n itemRefs.current[index] = el;\n }}\n role=\"option\"\n aria-selected={selectedIndex === index}\n className={`absolute top-1/2 cursor-pointer whitespace-nowrap leading-none will-change-[transform,opacity,filter] [font-size:var(--ow-font-size)] [color:color-mix(in_srgb,var(--ow-active-color)_calc(var(--ow-p,0)*100%),var(--ow-text-color))] ${\n side === 'right' ? 'right-[var(--ow-inset)] origin-right' : 'left-[var(--ow-inset)] origin-left'\n } ${selectedIndex === index ? 'font-medium' : 'font-extralight'}`}\n onClick={() => handleItemClick(index)}\n >\n {label}\n
\n ))}\n
\n );\n};\n\nexport default OptionWheel;\n"
}
],
"registryDependencies": [],
diff --git a/src/content/Animations/ImageTrail/ImageTrail.jsx b/src/content/Animations/ImageTrail/ImageTrail.jsx
index f8b4fdf57..a631e9e45 100644
--- a/src/content/Animations/ImageTrail/ImageTrail.jsx
+++ b/src/content/Animations/ImageTrail/ImageTrail.jsx
@@ -49,11 +49,17 @@ class ImageItem {
getRect() {
this.rect = this.DOM.el.getBoundingClientRect();
}
+
+ destroy() {
+ window.removeEventListener('resize', this.resize);
+ }
}
class ImageTrailVariant1 {
constructor(container) {
this.container = container;
+ this.rafId = null;
+ this.destroyed = false;
this.DOM = { el: container };
this.images = [...this.DOM.el.querySelectorAll('.content__img')].map(img => new ImageItem(img));
this.imagesTotal = this.images.length;
@@ -79,16 +85,20 @@ class ImageTrailVariant1 {
this.mousePos = getLocalPointerPos(ev, rect);
this.cacheMousePos = { ...this.mousePos };
- requestAnimationFrame(() => this.render());
+ this.rafId = requestAnimationFrame(() => this.render());
container.removeEventListener('mousemove', initRender);
container.removeEventListener('touchmove', initRender);
};
container.addEventListener('mousemove', initRender);
container.addEventListener('touchmove', initRender);
+ this.handlePointerMove = handlePointerMove;
+ this.initRender = initRender;
}
render() {
+ if (this.destroyed) return;
+
let distance = getMouseDistance(this.mousePos, this.lastMousePos);
this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.1);
this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.1);
@@ -100,7 +110,7 @@ class ImageTrailVariant1 {
if (this.isIdle && this.zIndexVal !== 1) {
this.zIndexVal = 1;
}
- requestAnimationFrame(() => this.render());
+ this.rafId = requestAnimationFrame(() => this.render());
}
showNextImage() {
@@ -143,6 +153,22 @@ class ImageTrailVariant1 {
);
}
+ destroy() {
+ this.destroyed = true;
+ if (this.rafId !== null) {
+ cancelAnimationFrame(this.rafId);
+ this.rafId = null;
+ }
+ this.container.removeEventListener('mousemove', this.handlePointerMove);
+ this.container.removeEventListener('touchmove', this.handlePointerMove);
+ this.container.removeEventListener('mousemove', this.initRender);
+ this.container.removeEventListener('touchmove', this.initRender);
+ this.images.forEach(img => {
+ gsap.killTweensOf(img.DOM.el);
+ img.destroy();
+ });
+ }
+
onImageActivated() {
this.activeImagesCount++;
this.isIdle = false;
@@ -158,6 +184,8 @@ class ImageTrailVariant1 {
class ImageTrailVariant2 {
constructor(container) {
this.container = container;
+ this.rafId = null;
+ this.destroyed = false;
this.DOM = { el: container };
this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img));
this.imagesTotal = this.images.length;
@@ -183,16 +211,20 @@ class ImageTrailVariant2 {
this.mousePos = getLocalPointerPos(ev, rect);
this.cacheMousePos = { ...this.mousePos };
- requestAnimationFrame(() => this.render());
+ this.rafId = requestAnimationFrame(() => this.render());
container.removeEventListener('mousemove', initRender);
container.removeEventListener('touchmove', initRender);
};
container.addEventListener('mousemove', initRender);
container.addEventListener('touchmove', initRender);
+ this.handlePointerMove = handlePointerMove;
+ this.initRender = initRender;
}
render() {
+ if (this.destroyed) return;
+
let distance = getMouseDistance(this.mousePos, this.lastMousePos);
this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.1);
this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.1);
@@ -204,7 +236,7 @@ class ImageTrailVariant2 {
if (this.isIdle && this.zIndexVal !== 1) {
this.zIndexVal = 1;
}
- requestAnimationFrame(() => this.render());
+ this.rafId = requestAnimationFrame(() => this.render());
}
showNextImage() {
@@ -262,6 +294,22 @@ class ImageTrailVariant2 {
);
}
+ destroy() {
+ this.destroyed = true;
+ if (this.rafId !== null) {
+ cancelAnimationFrame(this.rafId);
+ this.rafId = null;
+ }
+ this.container.removeEventListener('mousemove', this.handlePointerMove);
+ this.container.removeEventListener('touchmove', this.handlePointerMove);
+ this.container.removeEventListener('mousemove', this.initRender);
+ this.container.removeEventListener('touchmove', this.initRender);
+ this.images.forEach(img => {
+ gsap.killTweensOf(img.DOM.el);
+ img.destroy();
+ });
+ }
+
onImageActivated() {
this.activeImagesCount++;
this.isIdle = false;
@@ -275,6 +323,8 @@ class ImageTrailVariant2 {
class ImageTrailVariant3 {
constructor(container) {
this.container = container;
+ this.rafId = null;
+ this.destroyed = false;
this.DOM = { el: container };
this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img));
this.imagesTotal = this.images.length;
@@ -300,15 +350,19 @@ class ImageTrailVariant3 {
this.mousePos = getLocalPointerPos(ev, rect);
this.cacheMousePos = { ...this.mousePos };
- requestAnimationFrame(() => this.render());
+ this.rafId = requestAnimationFrame(() => this.render());
container.removeEventListener('mousemove', initRender);
container.removeEventListener('touchmove', initRender);
};
container.addEventListener('mousemove', initRender);
container.addEventListener('touchmove', initRender);
+ this.handlePointerMove = handlePointerMove;
+ this.initRender = initRender;
}
render() {
+ if (this.destroyed) return;
+
let distance = getMouseDistance(this.mousePos, this.lastMousePos);
this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.1);
this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.1);
@@ -320,7 +374,7 @@ class ImageTrailVariant3 {
if (this.isIdle && this.zIndexVal !== 1) {
this.zIndexVal = 1;
}
- requestAnimationFrame(() => this.render());
+ this.rafId = requestAnimationFrame(() => this.render());
}
showNextImage() {
@@ -380,6 +434,22 @@ class ImageTrailVariant3 {
);
}
+ destroy() {
+ this.destroyed = true;
+ if (this.rafId !== null) {
+ cancelAnimationFrame(this.rafId);
+ this.rafId = null;
+ }
+ this.container.removeEventListener('mousemove', this.handlePointerMove);
+ this.container.removeEventListener('touchmove', this.handlePointerMove);
+ this.container.removeEventListener('mousemove', this.initRender);
+ this.container.removeEventListener('touchmove', this.initRender);
+ this.images.forEach(img => {
+ gsap.killTweensOf(img.DOM.el);
+ img.destroy();
+ });
+ }
+
onImageActivated() {
this.activeImagesCount++;
this.isIdle = false;
@@ -393,6 +463,8 @@ class ImageTrailVariant3 {
class ImageTrailVariant4 {
constructor(container) {
this.container = container;
+ this.rafId = null;
+ this.destroyed = false;
this.DOM = { el: container };
this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img));
this.imagesTotal = this.images.length;
@@ -417,15 +489,19 @@ class ImageTrailVariant4 {
const rect = container.getBoundingClientRect();
this.mousePos = getLocalPointerPos(ev, rect);
this.cacheMousePos = { ...this.mousePos };
- requestAnimationFrame(() => this.render());
+ this.rafId = requestAnimationFrame(() => this.render());
container.removeEventListener('mousemove', initRender);
container.removeEventListener('touchmove', initRender);
};
container.addEventListener('mousemove', initRender);
container.addEventListener('touchmove', initRender);
+ this.handlePointerMove = handlePointerMove;
+ this.initRender = initRender;
}
render() {
+ if (this.destroyed) return;
+
let distance = getMouseDistance(this.mousePos, this.lastMousePos);
if (distance > this.threshold) {
this.showNextImage();
@@ -435,7 +511,7 @@ class ImageTrailVariant4 {
this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.1);
if (this.isIdle && this.zIndexVal !== 1) this.zIndexVal = 1;
- requestAnimationFrame(() => this.render());
+ this.rafId = requestAnimationFrame(() => this.render());
}
showNextImage() {
@@ -512,6 +588,22 @@ class ImageTrailVariant4 {
);
}
+ destroy() {
+ this.destroyed = true;
+ if (this.rafId !== null) {
+ cancelAnimationFrame(this.rafId);
+ this.rafId = null;
+ }
+ this.container.removeEventListener('mousemove', this.handlePointerMove);
+ this.container.removeEventListener('touchmove', this.handlePointerMove);
+ this.container.removeEventListener('mousemove', this.initRender);
+ this.container.removeEventListener('touchmove', this.initRender);
+ this.images.forEach(img => {
+ gsap.killTweensOf(img.DOM.el);
+ img.destroy();
+ });
+ }
+
onImageActivated() {
this.activeImagesCount++;
this.isIdle = false;
@@ -525,6 +617,8 @@ class ImageTrailVariant4 {
class ImageTrailVariant5 {
constructor(container) {
this.container = container;
+ this.rafId = null;
+ this.destroyed = false;
this.DOM = { el: container };
this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img));
this.imagesTotal = this.images.length;
@@ -550,15 +644,19 @@ class ImageTrailVariant5 {
const rect = container.getBoundingClientRect();
this.mousePos = getLocalPointerPos(ev, rect);
this.cacheMousePos = { ...this.mousePos };
- requestAnimationFrame(() => this.render());
+ this.rafId = requestAnimationFrame(() => this.render());
container.removeEventListener('mousemove', initRender);
container.removeEventListener('touchmove', initRender);
};
container.addEventListener('mousemove', initRender);
container.addEventListener('touchmove', initRender);
+ this.handlePointerMove = handlePointerMove;
+ this.initRender = initRender;
}
render() {
+ if (this.destroyed) return;
+
let distance = getMouseDistance(this.mousePos, this.lastMousePos);
if (distance > this.threshold) {
this.showNextImage();
@@ -567,7 +665,7 @@ class ImageTrailVariant5 {
this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.1);
this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.1);
if (this.isIdle && this.zIndexVal !== 1) this.zIndexVal = 1;
- requestAnimationFrame(() => this.render());
+ this.rafId = requestAnimationFrame(() => this.render());
}
showNextImage() {
@@ -640,6 +738,22 @@ class ImageTrailVariant5 {
);
}
+ destroy() {
+ this.destroyed = true;
+ if (this.rafId !== null) {
+ cancelAnimationFrame(this.rafId);
+ this.rafId = null;
+ }
+ this.container.removeEventListener('mousemove', this.handlePointerMove);
+ this.container.removeEventListener('touchmove', this.handlePointerMove);
+ this.container.removeEventListener('mousemove', this.initRender);
+ this.container.removeEventListener('touchmove', this.initRender);
+ this.images.forEach(img => {
+ gsap.killTweensOf(img.DOM.el);
+ img.destroy();
+ });
+ }
+
onImageActivated() {
this.activeImagesCount++;
this.isIdle = false;
@@ -653,6 +767,8 @@ class ImageTrailVariant5 {
class ImageTrailVariant6 {
constructor(container) {
this.container = container;
+ this.rafId = null;
+ this.destroyed = false;
this.DOM = { el: container };
this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img));
this.imagesTotal = this.images.length;
@@ -677,15 +793,19 @@ class ImageTrailVariant6 {
const rect = container.getBoundingClientRect();
this.mousePos = getLocalPointerPos(ev, rect);
this.cacheMousePos = { ...this.mousePos };
- requestAnimationFrame(() => this.render());
+ this.rafId = requestAnimationFrame(() => this.render());
container.removeEventListener('mousemove', initRender);
container.removeEventListener('touchmove', initRender);
};
container.addEventListener('mousemove', initRender);
container.addEventListener('touchmove', initRender);
+ this.handlePointerMove = handlePointerMove;
+ this.initRender = initRender;
}
render() {
+ if (this.destroyed) return;
+
let distance = getMouseDistance(this.mousePos, this.lastMousePos);
this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.3);
this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.3);
@@ -697,7 +817,7 @@ class ImageTrailVariant6 {
if (this.isIdle && this.zIndexVal !== 1) {
this.zIndexVal = 1;
}
- requestAnimationFrame(() => this.render());
+ this.rafId = requestAnimationFrame(() => this.render());
}
mapSpeedToSize(speed, minSize, maxSize) {
@@ -780,6 +900,22 @@ class ImageTrailVariant6 {
);
}
+ destroy() {
+ this.destroyed = true;
+ if (this.rafId !== null) {
+ cancelAnimationFrame(this.rafId);
+ this.rafId = null;
+ }
+ this.container.removeEventListener('mousemove', this.handlePointerMove);
+ this.container.removeEventListener('touchmove', this.handlePointerMove);
+ this.container.removeEventListener('mousemove', this.initRender);
+ this.container.removeEventListener('touchmove', this.initRender);
+ this.images.forEach(img => {
+ gsap.killTweensOf(img.DOM.el);
+ img.destroy();
+ });
+ }
+
onImageActivated() {
this.activeImagesCount++;
this.isIdle = false;
@@ -803,6 +939,8 @@ function getNewPosition(position, offset, arr) {
class ImageTrailVariant7 {
constructor(container) {
this.container = container;
+ this.rafId = null;
+ this.destroyed = false;
this.DOM = { el: container };
this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img));
this.imagesTotal = this.images.length;
@@ -831,15 +969,19 @@ class ImageTrailVariant7 {
const rect = container.getBoundingClientRect();
this.mousePos = getLocalPointerPos(ev, rect);
this.cacheMousePos = { ...this.mousePos };
- requestAnimationFrame(() => this.render());
+ this.rafId = requestAnimationFrame(() => this.render());
container.removeEventListener('mousemove', initRender);
container.removeEventListener('touchmove', initRender);
};
container.addEventListener('mousemove', initRender);
container.addEventListener('touchmove', initRender);
+ this.handlePointerMove = handlePointerMove;
+ this.initRender = initRender;
}
render() {
+ if (this.destroyed) return;
+
let distance = getMouseDistance(this.mousePos, this.lastMousePos);
this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.3);
this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.3);
@@ -850,7 +992,7 @@ class ImageTrailVariant7 {
}
if (this.isIdle && this.zIndexVal !== 1) this.zIndexVal = 1;
- requestAnimationFrame(() => this.render());
+ this.rafId = requestAnimationFrame(() => this.render());
}
showNextImage() {
@@ -905,6 +1047,22 @@ class ImageTrailVariant7 {
}
}
+ destroy() {
+ this.destroyed = true;
+ if (this.rafId !== null) {
+ cancelAnimationFrame(this.rafId);
+ this.rafId = null;
+ }
+ this.container.removeEventListener('mousemove', this.handlePointerMove);
+ this.container.removeEventListener('touchmove', this.handlePointerMove);
+ this.container.removeEventListener('mousemove', this.initRender);
+ this.container.removeEventListener('touchmove', this.initRender);
+ this.images.forEach(img => {
+ gsap.killTweensOf(img.DOM.el);
+ img.destroy();
+ });
+ }
+
onImageActivated() {
this.activeImagesCount++;
this.isIdle = false;
@@ -917,6 +1075,8 @@ class ImageTrailVariant7 {
class ImageTrailVariant8 {
constructor(container) {
this.container = container;
+ this.rafId = null;
+ this.destroyed = false;
this.DOM = { el: container };
this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img));
this.imagesTotal = this.images.length;
@@ -946,15 +1106,19 @@ class ImageTrailVariant8 {
const rect = container.getBoundingClientRect();
this.mousePos = getLocalPointerPos(ev, rect);
this.cacheMousePos = { ...this.mousePos };
- requestAnimationFrame(() => this.render());
+ this.rafId = requestAnimationFrame(() => this.render());
container.removeEventListener('mousemove', initRender);
container.removeEventListener('touchmove', initRender);
};
container.addEventListener('mousemove', initRender);
container.addEventListener('touchmove', initRender);
+ this.handlePointerMove = handlePointerMove;
+ this.initRender = initRender;
}
render() {
+ if (this.destroyed) return;
+
let distance = getMouseDistance(this.mousePos, this.lastMousePos);
this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.1);
this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.1);
@@ -966,7 +1130,7 @@ class ImageTrailVariant8 {
if (this.isIdle && this.zIndexVal !== 1) {
this.zIndexVal = 1;
}
- requestAnimationFrame(() => this.render());
+ this.rafId = requestAnimationFrame(() => this.render());
}
showNextImage() {
@@ -1035,6 +1199,22 @@ class ImageTrailVariant8 {
);
}
+ destroy() {
+ this.destroyed = true;
+ if (this.rafId !== null) {
+ cancelAnimationFrame(this.rafId);
+ this.rafId = null;
+ }
+ this.container.removeEventListener('mousemove', this.handlePointerMove);
+ this.container.removeEventListener('touchmove', this.handlePointerMove);
+ this.container.removeEventListener('mousemove', this.initRender);
+ this.container.removeEventListener('touchmove', this.initRender);
+ this.images.forEach(img => {
+ gsap.killTweensOf(img.DOM.el);
+ img.destroy();
+ });
+ }
+
onImageActivated() {
this.activeImagesCount++;
this.isIdle = false;
@@ -1063,7 +1243,11 @@ export default function ImageTrail({ items = [], variant = 1 }) {
if (!containerRef.current) return;
const Cls = variantMap[variant] || variantMap[1];
- new Cls(containerRef.current);
+ const instance = new Cls(containerRef.current);
+
+ return () => {
+ instance.destroy();
+ };
}, [variant, items]);
return (
diff --git a/src/tailwind/Animations/ImageTrail/ImageTrail.jsx b/src/tailwind/Animations/ImageTrail/ImageTrail.jsx
index 03b566294..5ddc752d7 100644
--- a/src/tailwind/Animations/ImageTrail/ImageTrail.jsx
+++ b/src/tailwind/Animations/ImageTrail/ImageTrail.jsx
@@ -47,11 +47,17 @@ class ImageItem {
getRect() {
this.rect = this.DOM.el.getBoundingClientRect();
}
+
+ destroy() {
+ window.removeEventListener('resize', this.resize);
+ }
}
class ImageTrailVariant1 {
constructor(container) {
this.container = container;
+ this.rafId = null;
+ this.destroyed = false;
this.DOM = { el: container };
this.images = [...this.DOM.el.querySelectorAll('.content__img')].map(img => new ImageItem(img));
this.imagesTotal = this.images.length;
@@ -77,16 +83,20 @@ class ImageTrailVariant1 {
this.mousePos = getLocalPointerPos(ev, rect);
this.cacheMousePos = { ...this.mousePos };
- requestAnimationFrame(() => this.render());
+ this.rafId = requestAnimationFrame(() => this.render());
container.removeEventListener('mousemove', initRender);
container.removeEventListener('touchmove', initRender);
};
container.addEventListener('mousemove', initRender);
container.addEventListener('touchmove', initRender);
+ this.handlePointerMove = handlePointerMove;
+ this.initRender = initRender;
}
render() {
+ if (this.destroyed) return;
+
let distance = getMouseDistance(this.mousePos, this.lastMousePos);
this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.1);
this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.1);
@@ -98,7 +108,7 @@ class ImageTrailVariant1 {
if (this.isIdle && this.zIndexVal !== 1) {
this.zIndexVal = 1;
}
- requestAnimationFrame(() => this.render());
+ this.rafId = requestAnimationFrame(() => this.render());
}
showNextImage() {
@@ -141,6 +151,22 @@ class ImageTrailVariant1 {
);
}
+ destroy() {
+ this.destroyed = true;
+ if (this.rafId !== null) {
+ cancelAnimationFrame(this.rafId);
+ this.rafId = null;
+ }
+ this.container.removeEventListener('mousemove', this.handlePointerMove);
+ this.container.removeEventListener('touchmove', this.handlePointerMove);
+ this.container.removeEventListener('mousemove', this.initRender);
+ this.container.removeEventListener('touchmove', this.initRender);
+ this.images.forEach(img => {
+ gsap.killTweensOf(img.DOM.el);
+ img.destroy();
+ });
+ }
+
onImageActivated() {
this.activeImagesCount++;
this.isIdle = false;
@@ -156,6 +182,8 @@ class ImageTrailVariant1 {
class ImageTrailVariant2 {
constructor(container) {
this.container = container;
+ this.rafId = null;
+ this.destroyed = false;
this.DOM = { el: container };
this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img));
this.imagesTotal = this.images.length;
@@ -181,16 +209,20 @@ class ImageTrailVariant2 {
this.mousePos = getLocalPointerPos(ev, rect);
this.cacheMousePos = { ...this.mousePos };
- requestAnimationFrame(() => this.render());
+ this.rafId = requestAnimationFrame(() => this.render());
container.removeEventListener('mousemove', initRender);
container.removeEventListener('touchmove', initRender);
};
container.addEventListener('mousemove', initRender);
container.addEventListener('touchmove', initRender);
+ this.handlePointerMove = handlePointerMove;
+ this.initRender = initRender;
}
render() {
+ if (this.destroyed) return;
+
let distance = getMouseDistance(this.mousePos, this.lastMousePos);
this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.1);
this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.1);
@@ -202,7 +234,7 @@ class ImageTrailVariant2 {
if (this.isIdle && this.zIndexVal !== 1) {
this.zIndexVal = 1;
}
- requestAnimationFrame(() => this.render());
+ this.rafId = requestAnimationFrame(() => this.render());
}
showNextImage() {
@@ -260,6 +292,22 @@ class ImageTrailVariant2 {
);
}
+ destroy() {
+ this.destroyed = true;
+ if (this.rafId !== null) {
+ cancelAnimationFrame(this.rafId);
+ this.rafId = null;
+ }
+ this.container.removeEventListener('mousemove', this.handlePointerMove);
+ this.container.removeEventListener('touchmove', this.handlePointerMove);
+ this.container.removeEventListener('mousemove', this.initRender);
+ this.container.removeEventListener('touchmove', this.initRender);
+ this.images.forEach(img => {
+ gsap.killTweensOf(img.DOM.el);
+ img.destroy();
+ });
+ }
+
onImageActivated() {
this.activeImagesCount++;
this.isIdle = false;
@@ -273,6 +321,8 @@ class ImageTrailVariant2 {
class ImageTrailVariant3 {
constructor(container) {
this.container = container;
+ this.rafId = null;
+ this.destroyed = false;
this.DOM = { el: container };
this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img));
this.imagesTotal = this.images.length;
@@ -298,15 +348,19 @@ class ImageTrailVariant3 {
this.mousePos = getLocalPointerPos(ev, rect);
this.cacheMousePos = { ...this.mousePos };
- requestAnimationFrame(() => this.render());
+ this.rafId = requestAnimationFrame(() => this.render());
container.removeEventListener('mousemove', initRender);
container.removeEventListener('touchmove', initRender);
};
container.addEventListener('mousemove', initRender);
container.addEventListener('touchmove', initRender);
+ this.handlePointerMove = handlePointerMove;
+ this.initRender = initRender;
}
render() {
+ if (this.destroyed) return;
+
let distance = getMouseDistance(this.mousePos, this.lastMousePos);
this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.1);
this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.1);
@@ -318,7 +372,7 @@ class ImageTrailVariant3 {
if (this.isIdle && this.zIndexVal !== 1) {
this.zIndexVal = 1;
}
- requestAnimationFrame(() => this.render());
+ this.rafId = requestAnimationFrame(() => this.render());
}
showNextImage() {
@@ -378,6 +432,22 @@ class ImageTrailVariant3 {
);
}
+ destroy() {
+ this.destroyed = true;
+ if (this.rafId !== null) {
+ cancelAnimationFrame(this.rafId);
+ this.rafId = null;
+ }
+ this.container.removeEventListener('mousemove', this.handlePointerMove);
+ this.container.removeEventListener('touchmove', this.handlePointerMove);
+ this.container.removeEventListener('mousemove', this.initRender);
+ this.container.removeEventListener('touchmove', this.initRender);
+ this.images.forEach(img => {
+ gsap.killTweensOf(img.DOM.el);
+ img.destroy();
+ });
+ }
+
onImageActivated() {
this.activeImagesCount++;
this.isIdle = false;
@@ -391,6 +461,8 @@ class ImageTrailVariant3 {
class ImageTrailVariant4 {
constructor(container) {
this.container = container;
+ this.rafId = null;
+ this.destroyed = false;
this.DOM = { el: container };
this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img));
this.imagesTotal = this.images.length;
@@ -415,15 +487,19 @@ class ImageTrailVariant4 {
const rect = container.getBoundingClientRect();
this.mousePos = getLocalPointerPos(ev, rect);
this.cacheMousePos = { ...this.mousePos };
- requestAnimationFrame(() => this.render());
+ this.rafId = requestAnimationFrame(() => this.render());
container.removeEventListener('mousemove', initRender);
container.removeEventListener('touchmove', initRender);
};
container.addEventListener('mousemove', initRender);
container.addEventListener('touchmove', initRender);
+ this.handlePointerMove = handlePointerMove;
+ this.initRender = initRender;
}
render() {
+ if (this.destroyed) return;
+
let distance = getMouseDistance(this.mousePos, this.lastMousePos);
if (distance > this.threshold) {
this.showNextImage();
@@ -433,7 +509,7 @@ class ImageTrailVariant4 {
this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.1);
if (this.isIdle && this.zIndexVal !== 1) this.zIndexVal = 1;
- requestAnimationFrame(() => this.render());
+ this.rafId = requestAnimationFrame(() => this.render());
}
showNextImage() {
@@ -510,6 +586,22 @@ class ImageTrailVariant4 {
);
}
+ destroy() {
+ this.destroyed = true;
+ if (this.rafId !== null) {
+ cancelAnimationFrame(this.rafId);
+ this.rafId = null;
+ }
+ this.container.removeEventListener('mousemove', this.handlePointerMove);
+ this.container.removeEventListener('touchmove', this.handlePointerMove);
+ this.container.removeEventListener('mousemove', this.initRender);
+ this.container.removeEventListener('touchmove', this.initRender);
+ this.images.forEach(img => {
+ gsap.killTweensOf(img.DOM.el);
+ img.destroy();
+ });
+ }
+
onImageActivated() {
this.activeImagesCount++;
this.isIdle = false;
@@ -523,6 +615,8 @@ class ImageTrailVariant4 {
class ImageTrailVariant5 {
constructor(container) {
this.container = container;
+ this.rafId = null;
+ this.destroyed = false;
this.DOM = { el: container };
this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img));
this.imagesTotal = this.images.length;
@@ -548,15 +642,19 @@ class ImageTrailVariant5 {
const rect = container.getBoundingClientRect();
this.mousePos = getLocalPointerPos(ev, rect);
this.cacheMousePos = { ...this.mousePos };
- requestAnimationFrame(() => this.render());
+ this.rafId = requestAnimationFrame(() => this.render());
container.removeEventListener('mousemove', initRender);
container.removeEventListener('touchmove', initRender);
};
container.addEventListener('mousemove', initRender);
container.addEventListener('touchmove', initRender);
+ this.handlePointerMove = handlePointerMove;
+ this.initRender = initRender;
}
render() {
+ if (this.destroyed) return;
+
let distance = getMouseDistance(this.mousePos, this.lastMousePos);
if (distance > this.threshold) {
this.showNextImage();
@@ -565,7 +663,7 @@ class ImageTrailVariant5 {
this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.1);
this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.1);
if (this.isIdle && this.zIndexVal !== 1) this.zIndexVal = 1;
- requestAnimationFrame(() => this.render());
+ this.rafId = requestAnimationFrame(() => this.render());
}
showNextImage() {
@@ -638,6 +736,22 @@ class ImageTrailVariant5 {
);
}
+ destroy() {
+ this.destroyed = true;
+ if (this.rafId !== null) {
+ cancelAnimationFrame(this.rafId);
+ this.rafId = null;
+ }
+ this.container.removeEventListener('mousemove', this.handlePointerMove);
+ this.container.removeEventListener('touchmove', this.handlePointerMove);
+ this.container.removeEventListener('mousemove', this.initRender);
+ this.container.removeEventListener('touchmove', this.initRender);
+ this.images.forEach(img => {
+ gsap.killTweensOf(img.DOM.el);
+ img.destroy();
+ });
+ }
+
onImageActivated() {
this.activeImagesCount++;
this.isIdle = false;
@@ -651,6 +765,8 @@ class ImageTrailVariant5 {
class ImageTrailVariant6 {
constructor(container) {
this.container = container;
+ this.rafId = null;
+ this.destroyed = false;
this.DOM = { el: container };
this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img));
this.imagesTotal = this.images.length;
@@ -675,15 +791,19 @@ class ImageTrailVariant6 {
const rect = container.getBoundingClientRect();
this.mousePos = getLocalPointerPos(ev, rect);
this.cacheMousePos = { ...this.mousePos };
- requestAnimationFrame(() => this.render());
+ this.rafId = requestAnimationFrame(() => this.render());
container.removeEventListener('mousemove', initRender);
container.removeEventListener('touchmove', initRender);
};
container.addEventListener('mousemove', initRender);
container.addEventListener('touchmove', initRender);
+ this.handlePointerMove = handlePointerMove;
+ this.initRender = initRender;
}
render() {
+ if (this.destroyed) return;
+
let distance = getMouseDistance(this.mousePos, this.lastMousePos);
this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.3);
this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.3);
@@ -695,7 +815,7 @@ class ImageTrailVariant6 {
if (this.isIdle && this.zIndexVal !== 1) {
this.zIndexVal = 1;
}
- requestAnimationFrame(() => this.render());
+ this.rafId = requestAnimationFrame(() => this.render());
}
mapSpeedToSize(speed, minSize, maxSize) {
@@ -778,6 +898,22 @@ class ImageTrailVariant6 {
);
}
+ destroy() {
+ this.destroyed = true;
+ if (this.rafId !== null) {
+ cancelAnimationFrame(this.rafId);
+ this.rafId = null;
+ }
+ this.container.removeEventListener('mousemove', this.handlePointerMove);
+ this.container.removeEventListener('touchmove', this.handlePointerMove);
+ this.container.removeEventListener('mousemove', this.initRender);
+ this.container.removeEventListener('touchmove', this.initRender);
+ this.images.forEach(img => {
+ gsap.killTweensOf(img.DOM.el);
+ img.destroy();
+ });
+ }
+
onImageActivated() {
this.activeImagesCount++;
this.isIdle = false;
@@ -801,6 +937,8 @@ function getNewPosition(position, offset, arr) {
class ImageTrailVariant7 {
constructor(container) {
this.container = container;
+ this.rafId = null;
+ this.destroyed = false;
this.DOM = { el: container };
this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img));
this.imagesTotal = this.images.length;
@@ -829,15 +967,19 @@ class ImageTrailVariant7 {
const rect = container.getBoundingClientRect();
this.mousePos = getLocalPointerPos(ev, rect);
this.cacheMousePos = { ...this.mousePos };
- requestAnimationFrame(() => this.render());
+ this.rafId = requestAnimationFrame(() => this.render());
container.removeEventListener('mousemove', initRender);
container.removeEventListener('touchmove', initRender);
};
container.addEventListener('mousemove', initRender);
container.addEventListener('touchmove', initRender);
+ this.handlePointerMove = handlePointerMove;
+ this.initRender = initRender;
}
render() {
+ if (this.destroyed) return;
+
let distance = getMouseDistance(this.mousePos, this.lastMousePos);
this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.3);
this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.3);
@@ -848,7 +990,7 @@ class ImageTrailVariant7 {
}
if (this.isIdle && this.zIndexVal !== 1) this.zIndexVal = 1;
- requestAnimationFrame(() => this.render());
+ this.rafId = requestAnimationFrame(() => this.render());
}
showNextImage() {
@@ -903,6 +1045,22 @@ class ImageTrailVariant7 {
}
}
+ destroy() {
+ this.destroyed = true;
+ if (this.rafId !== null) {
+ cancelAnimationFrame(this.rafId);
+ this.rafId = null;
+ }
+ this.container.removeEventListener('mousemove', this.handlePointerMove);
+ this.container.removeEventListener('touchmove', this.handlePointerMove);
+ this.container.removeEventListener('mousemove', this.initRender);
+ this.container.removeEventListener('touchmove', this.initRender);
+ this.images.forEach(img => {
+ gsap.killTweensOf(img.DOM.el);
+ img.destroy();
+ });
+ }
+
onImageActivated() {
this.activeImagesCount++;
this.isIdle = false;
@@ -915,6 +1073,8 @@ class ImageTrailVariant7 {
class ImageTrailVariant8 {
constructor(container) {
this.container = container;
+ this.rafId = null;
+ this.destroyed = false;
this.DOM = { el: container };
this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img));
this.imagesTotal = this.images.length;
@@ -944,15 +1104,19 @@ class ImageTrailVariant8 {
const rect = container.getBoundingClientRect();
this.mousePos = getLocalPointerPos(ev, rect);
this.cacheMousePos = { ...this.mousePos };
- requestAnimationFrame(() => this.render());
+ this.rafId = requestAnimationFrame(() => this.render());
container.removeEventListener('mousemove', initRender);
container.removeEventListener('touchmove', initRender);
};
container.addEventListener('mousemove', initRender);
container.addEventListener('touchmove', initRender);
+ this.handlePointerMove = handlePointerMove;
+ this.initRender = initRender;
}
render() {
+ if (this.destroyed) return;
+
let distance = getMouseDistance(this.mousePos, this.lastMousePos);
this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.1);
this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.1);
@@ -964,7 +1128,7 @@ class ImageTrailVariant8 {
if (this.isIdle && this.zIndexVal !== 1) {
this.zIndexVal = 1;
}
- requestAnimationFrame(() => this.render());
+ this.rafId = requestAnimationFrame(() => this.render());
}
showNextImage() {
@@ -1033,6 +1197,22 @@ class ImageTrailVariant8 {
);
}
+ destroy() {
+ this.destroyed = true;
+ if (this.rafId !== null) {
+ cancelAnimationFrame(this.rafId);
+ this.rafId = null;
+ }
+ this.container.removeEventListener('mousemove', this.handlePointerMove);
+ this.container.removeEventListener('touchmove', this.handlePointerMove);
+ this.container.removeEventListener('mousemove', this.initRender);
+ this.container.removeEventListener('touchmove', this.initRender);
+ this.images.forEach(img => {
+ gsap.killTweensOf(img.DOM.el);
+ img.destroy();
+ });
+ }
+
onImageActivated() {
this.activeImagesCount++;
this.isIdle = false;
@@ -1061,7 +1241,11 @@ export default function ImageTrail({ items = [], variant = 1 }) {
if (!containerRef.current) return;
const Cls = variantMap[variant] || variantMap[1];
- new Cls(containerRef.current);
+ const instance = new Cls(containerRef.current);
+
+ return () => {
+ instance.destroy();
+ };
}, [variant, items]);
return (
diff --git a/src/ts-default/Animations/ImageTrail/ImageTrail.tsx b/src/ts-default/Animations/ImageTrail/ImageTrail.tsx
index c789cf7ed..dd796507e 100644
--- a/src/ts-default/Animations/ImageTrail/ImageTrail.tsx
+++ b/src/ts-default/Animations/ImageTrail/ImageTrail.tsx
@@ -55,6 +55,10 @@ class ImageItem {
private getRect() {
this.rect = this.DOM.el.getBoundingClientRect();
}
+
+ public destroy() {
+ window.removeEventListener('resize', this.resize);
+ }
}
class ImageTrailVariant1 {
@@ -70,6 +74,10 @@ class ImageTrailVariant1 {
private mousePos: { x: number; y: number };
private lastMousePos: { x: number; y: number };
private cacheMousePos: { x: number; y: number };
+ private rafId: number | null = null;
+ private destroyed = false;
+ private handlePointerMove!: (ev: MouseEvent | TouchEvent) => void;
+ private initRender!: (ev: MouseEvent | TouchEvent) => void;
constructor(container: HTMLDivElement) {
this.container = container;
@@ -96,15 +104,19 @@ class ImageTrailVariant1 {
const rect = this.container.getBoundingClientRect();
this.mousePos = getLocalPointerPos(ev, rect);
this.cacheMousePos = { ...this.mousePos };
- requestAnimationFrame(() => this.render());
+ this.rafId = requestAnimationFrame(() => this.render());
container.removeEventListener('mousemove', initRender as EventListener);
container.removeEventListener('touchmove', initRender as EventListener);
};
container.addEventListener('mousemove', initRender as EventListener);
container.addEventListener('touchmove', initRender as EventListener);
+ this.handlePointerMove = handlePointerMove;
+ this.initRender = initRender;
}
private render() {
+ if (this.destroyed) return;
+
const distance = getMouseDistance(this.mousePos, this.lastMousePos);
this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.1);
this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.1);
@@ -116,7 +128,7 @@ class ImageTrailVariant1 {
if (this.isIdle && this.zIndexVal !== 1) {
this.zIndexVal = 1;
}
- requestAnimationFrame(() => this.render());
+ this.rafId = requestAnimationFrame(() => this.render());
}
private showNextImage() {
@@ -159,6 +171,22 @@ class ImageTrailVariant1 {
);
}
+ public destroy() {
+ this.destroyed = true;
+ if (this.rafId !== null) {
+ cancelAnimationFrame(this.rafId);
+ this.rafId = null;
+ }
+ this.container.removeEventListener('mousemove', this.handlePointerMove as EventListener);
+ this.container.removeEventListener('touchmove', this.handlePointerMove as EventListener);
+ this.container.removeEventListener('mousemove', this.initRender as EventListener);
+ this.container.removeEventListener('touchmove', this.initRender as EventListener);
+ this.images.forEach(img => {
+ gsap.killTweensOf(img.DOM.el);
+ img.destroy();
+ });
+ }
+
private onImageActivated() {
this.activeImagesCount++;
this.isIdle = false;
@@ -185,6 +213,10 @@ class ImageTrailVariant2 {
private mousePos: { x: number; y: number };
private lastMousePos: { x: number; y: number };
private cacheMousePos: { x: number; y: number };
+ private rafId: number | null = null;
+ private destroyed = false;
+ private handlePointerMove!: (ev: MouseEvent | TouchEvent) => void;
+ private initRender!: (ev: MouseEvent | TouchEvent) => void;
constructor(container: HTMLDivElement) {
this.container = container;
@@ -211,15 +243,19 @@ class ImageTrailVariant2 {
const rect = container.getBoundingClientRect();
this.mousePos = getLocalPointerPos(ev, rect);
this.cacheMousePos = { ...this.mousePos };
- requestAnimationFrame(() => this.render());
+ this.rafId = requestAnimationFrame(() => this.render());
container.removeEventListener('mousemove', initRender as EventListener);
container.removeEventListener('touchmove', initRender as EventListener);
};
container.addEventListener('mousemove', initRender as EventListener);
container.addEventListener('touchmove', initRender as EventListener);
+ this.handlePointerMove = handlePointerMove;
+ this.initRender = initRender;
}
private render() {
+ if (this.destroyed) return;
+
const distance = getMouseDistance(this.mousePos, this.lastMousePos);
this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.1);
this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.1);
@@ -231,7 +267,7 @@ class ImageTrailVariant2 {
if (this.isIdle && this.zIndexVal !== 1) {
this.zIndexVal = 1;
}
- requestAnimationFrame(() => this.render());
+ this.rafId = requestAnimationFrame(() => this.render());
}
private showNextImage() {
@@ -286,6 +322,22 @@ class ImageTrailVariant2 {
);
}
+ public destroy() {
+ this.destroyed = true;
+ if (this.rafId !== null) {
+ cancelAnimationFrame(this.rafId);
+ this.rafId = null;
+ }
+ this.container.removeEventListener('mousemove', this.handlePointerMove as EventListener);
+ this.container.removeEventListener('touchmove', this.handlePointerMove as EventListener);
+ this.container.removeEventListener('mousemove', this.initRender as EventListener);
+ this.container.removeEventListener('touchmove', this.initRender as EventListener);
+ this.images.forEach(img => {
+ gsap.killTweensOf(img.DOM.el);
+ img.destroy();
+ });
+ }
+
private onImageActivated() {
this.activeImagesCount++;
this.isIdle = false;
@@ -312,6 +364,10 @@ class ImageTrailVariant3 {
private mousePos: { x: number; y: number };
private lastMousePos: { x: number; y: number };
private cacheMousePos: { x: number; y: number };
+ private rafId: number | null = null;
+ private destroyed = false;
+ private handlePointerMove!: (ev: MouseEvent | TouchEvent) => void;
+ private initRender!: (ev: MouseEvent | TouchEvent) => void;
constructor(container: HTMLDivElement) {
this.container = container;
@@ -338,15 +394,19 @@ class ImageTrailVariant3 {
const rect = container.getBoundingClientRect();
this.mousePos = getLocalPointerPos(ev, rect);
this.cacheMousePos = { ...this.mousePos };
- requestAnimationFrame(() => this.render());
+ this.rafId = requestAnimationFrame(() => this.render());
container.removeEventListener('mousemove', initRender as EventListener);
container.removeEventListener('touchmove', initRender as EventListener);
};
container.addEventListener('mousemove', initRender as EventListener);
container.addEventListener('touchmove', initRender as EventListener);
+ this.handlePointerMove = handlePointerMove;
+ this.initRender = initRender;
}
private render() {
+ if (this.destroyed) return;
+
const distance = getMouseDistance(this.mousePos, this.lastMousePos);
this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.1);
this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.1);
@@ -358,7 +418,7 @@ class ImageTrailVariant3 {
if (this.isIdle && this.zIndexVal !== 1) {
this.zIndexVal = 1;
}
- requestAnimationFrame(() => this.render());
+ this.rafId = requestAnimationFrame(() => this.render());
}
private showNextImage() {
@@ -416,6 +476,22 @@ class ImageTrailVariant3 {
);
}
+ public destroy() {
+ this.destroyed = true;
+ if (this.rafId !== null) {
+ cancelAnimationFrame(this.rafId);
+ this.rafId = null;
+ }
+ this.container.removeEventListener('mousemove', this.handlePointerMove as EventListener);
+ this.container.removeEventListener('touchmove', this.handlePointerMove as EventListener);
+ this.container.removeEventListener('mousemove', this.initRender as EventListener);
+ this.container.removeEventListener('touchmove', this.initRender as EventListener);
+ this.images.forEach(img => {
+ gsap.killTweensOf(img.DOM.el);
+ img.destroy();
+ });
+ }
+
private onImageActivated() {
this.activeImagesCount++;
this.isIdle = false;
@@ -442,6 +518,10 @@ class ImageTrailVariant4 {
private mousePos: { x: number; y: number };
private lastMousePos: { x: number; y: number };
private cacheMousePos: { x: number; y: number };
+ private rafId: number | null = null;
+ private destroyed = false;
+ private handlePointerMove!: (ev: MouseEvent | TouchEvent) => void;
+ private initRender!: (ev: MouseEvent | TouchEvent) => void;
constructor(container: HTMLDivElement) {
this.container = container;
@@ -468,15 +548,19 @@ class ImageTrailVariant4 {
const rect = container.getBoundingClientRect();
this.mousePos = getLocalPointerPos(ev, rect);
this.cacheMousePos = { ...this.mousePos };
- requestAnimationFrame(() => this.render());
+ this.rafId = requestAnimationFrame(() => this.render());
container.removeEventListener('mousemove', initRender as EventListener);
container.removeEventListener('touchmove', initRender as EventListener);
};
container.addEventListener('mousemove', initRender as EventListener);
container.addEventListener('touchmove', initRender as EventListener);
+ this.handlePointerMove = handlePointerMove;
+ this.initRender = initRender;
}
private render() {
+ if (this.destroyed) return;
+
const distance = getMouseDistance(this.mousePos, this.lastMousePos);
if (distance > this.threshold) {
this.showNextImage();
@@ -486,7 +570,7 @@ class ImageTrailVariant4 {
this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.1);
if (this.isIdle && this.zIndexVal !== 1) this.zIndexVal = 1;
- requestAnimationFrame(() => this.render());
+ this.rafId = requestAnimationFrame(() => this.render());
}
private showNextImage() {
@@ -566,6 +650,22 @@ class ImageTrailVariant4 {
);
}
+ public destroy() {
+ this.destroyed = true;
+ if (this.rafId !== null) {
+ cancelAnimationFrame(this.rafId);
+ this.rafId = null;
+ }
+ this.container.removeEventListener('mousemove', this.handlePointerMove as EventListener);
+ this.container.removeEventListener('touchmove', this.handlePointerMove as EventListener);
+ this.container.removeEventListener('mousemove', this.initRender as EventListener);
+ this.container.removeEventListener('touchmove', this.initRender as EventListener);
+ this.images.forEach(img => {
+ gsap.killTweensOf(img.DOM.el);
+ img.destroy();
+ });
+ }
+
private onImageActivated() {
this.activeImagesCount++;
this.isIdle = false;
@@ -592,6 +692,10 @@ class ImageTrailVariant5 {
private mousePos: { x: number; y: number };
private lastMousePos: { x: number; y: number };
private cacheMousePos: { x: number; y: number };
+ private rafId: number | null = null;
+ private destroyed = false;
+ private handlePointerMove!: (ev: MouseEvent | TouchEvent) => void;
+ private initRender!: (ev: MouseEvent | TouchEvent) => void;
private lastAngle: number;
constructor(container: HTMLDivElement) {
@@ -620,15 +724,19 @@ class ImageTrailVariant5 {
const rect = container.getBoundingClientRect();
this.mousePos = getLocalPointerPos(ev, rect);
this.cacheMousePos = { ...this.mousePos };
- requestAnimationFrame(() => this.render());
+ this.rafId = requestAnimationFrame(() => this.render());
container.removeEventListener('mousemove', initRender as EventListener);
container.removeEventListener('touchmove', initRender as EventListener);
};
container.addEventListener('mousemove', initRender as EventListener);
container.addEventListener('touchmove', initRender as EventListener);
+ this.handlePointerMove = handlePointerMove;
+ this.initRender = initRender;
}
private render() {
+ if (this.destroyed) return;
+
const distance = getMouseDistance(this.mousePos, this.lastMousePos);
if (distance > this.threshold) {
this.showNextImage();
@@ -637,7 +745,7 @@ class ImageTrailVariant5 {
this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.1);
this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.1);
if (this.isIdle && this.zIndexVal !== 1) this.zIndexVal = 1;
- requestAnimationFrame(() => this.render());
+ this.rafId = requestAnimationFrame(() => this.render());
}
private showNextImage() {
@@ -710,6 +818,22 @@ class ImageTrailVariant5 {
);
}
+ public destroy() {
+ this.destroyed = true;
+ if (this.rafId !== null) {
+ cancelAnimationFrame(this.rafId);
+ this.rafId = null;
+ }
+ this.container.removeEventListener('mousemove', this.handlePointerMove as EventListener);
+ this.container.removeEventListener('touchmove', this.handlePointerMove as EventListener);
+ this.container.removeEventListener('mousemove', this.initRender as EventListener);
+ this.container.removeEventListener('touchmove', this.initRender as EventListener);
+ this.images.forEach(img => {
+ gsap.killTweensOf(img.DOM.el);
+ img.destroy();
+ });
+ }
+
private onImageActivated() {
this.activeImagesCount++;
this.isIdle = false;
@@ -734,6 +858,10 @@ class ImageTrailVariant6 {
private mousePos: { x: number; y: number };
private lastMousePos: { x: number; y: number };
private cacheMousePos: { x: number; y: number };
+ private rafId: number | null = null;
+ private destroyed = false;
+ private handlePointerMove!: (ev: MouseEvent | TouchEvent) => void;
+ private initRender!: (ev: MouseEvent | TouchEvent) => void;
constructor(container: HTMLDivElement) {
this.container = container;
@@ -760,15 +888,19 @@ class ImageTrailVariant6 {
const rect = container.getBoundingClientRect();
this.mousePos = getLocalPointerPos(ev, rect);
this.cacheMousePos = { ...this.mousePos };
- requestAnimationFrame(() => this.render());
+ this.rafId = requestAnimationFrame(() => this.render());
container.removeEventListener('mousemove', initRender as EventListener);
container.removeEventListener('touchmove', initRender as EventListener);
};
container.addEventListener('mousemove', initRender as EventListener);
container.addEventListener('touchmove', initRender as EventListener);
+ this.handlePointerMove = handlePointerMove;
+ this.initRender = initRender;
}
private render() {
+ if (this.destroyed) return;
+
const distance = getMouseDistance(this.mousePos, this.lastMousePos);
this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.3);
this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.3);
@@ -780,7 +912,7 @@ class ImageTrailVariant6 {
if (this.isIdle && this.zIndexVal !== 1) {
this.zIndexVal = 1;
}
- requestAnimationFrame(() => this.render());
+ this.rafId = requestAnimationFrame(() => this.render());
}
private mapSpeedToSize(speed: number, minSize: number, maxSize: number) {
@@ -864,6 +996,22 @@ class ImageTrailVariant6 {
);
}
+ public destroy() {
+ this.destroyed = true;
+ if (this.rafId !== null) {
+ cancelAnimationFrame(this.rafId);
+ this.rafId = null;
+ }
+ this.container.removeEventListener('mousemove', this.handlePointerMove as EventListener);
+ this.container.removeEventListener('touchmove', this.handlePointerMove as EventListener);
+ this.container.removeEventListener('mousemove', this.initRender as EventListener);
+ this.container.removeEventListener('touchmove', this.initRender as EventListener);
+ this.images.forEach(img => {
+ gsap.killTweensOf(img.DOM.el);
+ img.destroy();
+ });
+ }
+
private onImageActivated() {
this.activeImagesCount++;
this.isIdle = false;
@@ -899,6 +1047,10 @@ class ImageTrailVariant7 {
private mousePos: { x: number; y: number };
private lastMousePos: { x: number; y: number };
private cacheMousePos: { x: number; y: number };
+ private rafId: number | null = null;
+ private destroyed = false;
+ private handlePointerMove!: (ev: MouseEvent | TouchEvent) => void;
+ private initRender!: (ev: MouseEvent | TouchEvent) => void;
private visibleImagesCount: number;
private visibleImagesTotal: number;
@@ -930,15 +1082,19 @@ class ImageTrailVariant7 {
const rect = container.getBoundingClientRect();
this.mousePos = getLocalPointerPos(ev, rect);
this.cacheMousePos = { ...this.mousePos };
- requestAnimationFrame(() => this.render());
+ this.rafId = requestAnimationFrame(() => this.render());
container.removeEventListener('mousemove', initRender as EventListener);
container.removeEventListener('touchmove', initRender as EventListener);
};
container.addEventListener('mousemove', initRender as EventListener);
container.addEventListener('touchmove', initRender as EventListener);
+ this.handlePointerMove = handlePointerMove;
+ this.initRender = initRender;
}
private render() {
+ if (this.destroyed) return;
+
const distance = getMouseDistance(this.mousePos, this.lastMousePos);
this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.3);
this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.3);
@@ -949,7 +1105,7 @@ class ImageTrailVariant7 {
}
if (this.isIdle && this.zIndexVal !== 1) this.zIndexVal = 1;
- requestAnimationFrame(() => this.render());
+ this.rafId = requestAnimationFrame(() => this.render());
}
private showNextImage() {
@@ -1004,6 +1160,22 @@ class ImageTrailVariant7 {
}
}
+ public destroy() {
+ this.destroyed = true;
+ if (this.rafId !== null) {
+ cancelAnimationFrame(this.rafId);
+ this.rafId = null;
+ }
+ this.container.removeEventListener('mousemove', this.handlePointerMove as EventListener);
+ this.container.removeEventListener('touchmove', this.handlePointerMove as EventListener);
+ this.container.removeEventListener('mousemove', this.initRender as EventListener);
+ this.container.removeEventListener('touchmove', this.initRender as EventListener);
+ this.images.forEach(img => {
+ gsap.killTweensOf(img.DOM.el);
+ img.destroy();
+ });
+ }
+
private onImageActivated() {
this.activeImagesCount++;
this.isIdle = false;
@@ -1027,6 +1199,10 @@ class ImageTrailVariant8 {
private mousePos: { x: number; y: number };
private lastMousePos: { x: number; y: number };
private cacheMousePos: { x: number; y: number };
+ private rafId: number | null = null;
+ private destroyed = false;
+ private handlePointerMove!: (ev: MouseEvent | TouchEvent) => void;
+ private initRender!: (ev: MouseEvent | TouchEvent) => void;
private rotation: { x: number; y: number };
private cachedRotation: { x: number; y: number };
private zValue: number;
@@ -1061,15 +1237,19 @@ class ImageTrailVariant8 {
const rect = container.getBoundingClientRect();
this.mousePos = getLocalPointerPos(ev, rect);
this.cacheMousePos = { ...this.mousePos };
- requestAnimationFrame(() => this.render());
+ this.rafId = requestAnimationFrame(() => this.render());
container.removeEventListener('mousemove', initRender as EventListener);
container.removeEventListener('touchmove', initRender as EventListener);
};
container.addEventListener('mousemove', initRender as EventListener);
container.addEventListener('touchmove', initRender as EventListener);
+ this.handlePointerMove = handlePointerMove;
+ this.initRender = initRender;
}
private render() {
+ if (this.destroyed) return;
+
const distance = getMouseDistance(this.mousePos, this.lastMousePos);
this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.1);
this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.1);
@@ -1081,7 +1261,7 @@ class ImageTrailVariant8 {
if (this.isIdle && this.zIndexVal !== 1) {
this.zIndexVal = 1;
}
- requestAnimationFrame(() => this.render());
+ this.rafId = requestAnimationFrame(() => this.render());
}
private showNextImage() {
@@ -1150,6 +1330,22 @@ class ImageTrailVariant8 {
);
}
+ public destroy() {
+ this.destroyed = true;
+ if (this.rafId !== null) {
+ cancelAnimationFrame(this.rafId);
+ this.rafId = null;
+ }
+ this.container.removeEventListener('mousemove', this.handlePointerMove as EventListener);
+ this.container.removeEventListener('touchmove', this.handlePointerMove as EventListener);
+ this.container.removeEventListener('mousemove', this.initRender as EventListener);
+ this.container.removeEventListener('touchmove', this.initRender as EventListener);
+ this.images.forEach(img => {
+ gsap.killTweensOf(img.DOM.el);
+ img.destroy();
+ });
+ }
+
private onImageActivated() {
this.activeImagesCount++;
this.isIdle = false;
@@ -1195,7 +1391,11 @@ export default function ImageTrail({ items = [], variant = 1 }: ImageTrailProps)
useEffect(() => {
if (!containerRef.current) return;
const Cls = variantMap[variant] || variantMap[1];
- new Cls(containerRef.current);
+ const instance = new Cls(containerRef.current);
+
+ return () => {
+ instance.destroy();
+ };
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [variant, items]);
diff --git a/src/ts-tailwind/Animations/ImageTrail/ImageTrail.tsx b/src/ts-tailwind/Animations/ImageTrail/ImageTrail.tsx
index aada4cfe4..f0d40c37e 100644
--- a/src/ts-tailwind/Animations/ImageTrail/ImageTrail.tsx
+++ b/src/ts-tailwind/Animations/ImageTrail/ImageTrail.tsx
@@ -54,6 +54,10 @@ class ImageItem {
private getRect() {
this.rect = this.DOM.el.getBoundingClientRect();
}
+
+ public destroy() {
+ window.removeEventListener('resize', this.resize);
+ }
}
class ImageTrailVariant1 {
@@ -69,6 +73,10 @@ class ImageTrailVariant1 {
private mousePos: { x: number; y: number };
private lastMousePos: { x: number; y: number };
private cacheMousePos: { x: number; y: number };
+ private rafId: number | null = null;
+ private destroyed = false;
+ private handlePointerMove!: (ev: MouseEvent | TouchEvent) => void;
+ private initRender!: (ev: MouseEvent | TouchEvent) => void;
constructor(container: HTMLDivElement) {
this.container = container;
@@ -95,15 +103,19 @@ class ImageTrailVariant1 {
const rect = this.container.getBoundingClientRect();
this.mousePos = getLocalPointerPos(ev, rect);
this.cacheMousePos = { ...this.mousePos };
- requestAnimationFrame(() => this.render());
+ this.rafId = requestAnimationFrame(() => this.render());
container.removeEventListener('mousemove', initRender as EventListener);
container.removeEventListener('touchmove', initRender as EventListener);
};
container.addEventListener('mousemove', initRender as EventListener);
container.addEventListener('touchmove', initRender as EventListener);
+ this.handlePointerMove = handlePointerMove;
+ this.initRender = initRender;
}
private render() {
+ if (this.destroyed) return;
+
const distance = getMouseDistance(this.mousePos, this.lastMousePos);
this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.1);
this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.1);
@@ -115,7 +127,7 @@ class ImageTrailVariant1 {
if (this.isIdle && this.zIndexVal !== 1) {
this.zIndexVal = 1;
}
- requestAnimationFrame(() => this.render());
+ this.rafId = requestAnimationFrame(() => this.render());
}
private showNextImage() {
@@ -158,6 +170,22 @@ class ImageTrailVariant1 {
);
}
+ public destroy() {
+ this.destroyed = true;
+ if (this.rafId !== null) {
+ cancelAnimationFrame(this.rafId);
+ this.rafId = null;
+ }
+ this.container.removeEventListener('mousemove', this.handlePointerMove as EventListener);
+ this.container.removeEventListener('touchmove', this.handlePointerMove as EventListener);
+ this.container.removeEventListener('mousemove', this.initRender as EventListener);
+ this.container.removeEventListener('touchmove', this.initRender as EventListener);
+ this.images.forEach(img => {
+ gsap.killTweensOf(img.DOM.el);
+ img.destroy();
+ });
+ }
+
private onImageActivated() {
this.activeImagesCount++;
this.isIdle = false;
@@ -184,6 +212,10 @@ class ImageTrailVariant2 {
private mousePos: { x: number; y: number };
private lastMousePos: { x: number; y: number };
private cacheMousePos: { x: number; y: number };
+ private rafId: number | null = null;
+ private destroyed = false;
+ private handlePointerMove!: (ev: MouseEvent | TouchEvent) => void;
+ private initRender!: (ev: MouseEvent | TouchEvent) => void;
constructor(container: HTMLDivElement) {
this.container = container;
@@ -210,15 +242,19 @@ class ImageTrailVariant2 {
const rect = container.getBoundingClientRect();
this.mousePos = getLocalPointerPos(ev, rect);
this.cacheMousePos = { ...this.mousePos };
- requestAnimationFrame(() => this.render());
+ this.rafId = requestAnimationFrame(() => this.render());
container.removeEventListener('mousemove', initRender as EventListener);
container.removeEventListener('touchmove', initRender as EventListener);
};
container.addEventListener('mousemove', initRender as EventListener);
container.addEventListener('touchmove', initRender as EventListener);
+ this.handlePointerMove = handlePointerMove;
+ this.initRender = initRender;
}
private render() {
+ if (this.destroyed) return;
+
const distance = getMouseDistance(this.mousePos, this.lastMousePos);
this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.1);
this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.1);
@@ -230,7 +266,7 @@ class ImageTrailVariant2 {
if (this.isIdle && this.zIndexVal !== 1) {
this.zIndexVal = 1;
}
- requestAnimationFrame(() => this.render());
+ this.rafId = requestAnimationFrame(() => this.render());
}
private showNextImage() {
@@ -285,6 +321,22 @@ class ImageTrailVariant2 {
);
}
+ public destroy() {
+ this.destroyed = true;
+ if (this.rafId !== null) {
+ cancelAnimationFrame(this.rafId);
+ this.rafId = null;
+ }
+ this.container.removeEventListener('mousemove', this.handlePointerMove as EventListener);
+ this.container.removeEventListener('touchmove', this.handlePointerMove as EventListener);
+ this.container.removeEventListener('mousemove', this.initRender as EventListener);
+ this.container.removeEventListener('touchmove', this.initRender as EventListener);
+ this.images.forEach(img => {
+ gsap.killTweensOf(img.DOM.el);
+ img.destroy();
+ });
+ }
+
private onImageActivated() {
this.activeImagesCount++;
this.isIdle = false;
@@ -311,6 +363,10 @@ class ImageTrailVariant3 {
private mousePos: { x: number; y: number };
private lastMousePos: { x: number; y: number };
private cacheMousePos: { x: number; y: number };
+ private rafId: number | null = null;
+ private destroyed = false;
+ private handlePointerMove!: (ev: MouseEvent | TouchEvent) => void;
+ private initRender!: (ev: MouseEvent | TouchEvent) => void;
constructor(container: HTMLDivElement) {
this.container = container;
@@ -337,15 +393,19 @@ class ImageTrailVariant3 {
const rect = container.getBoundingClientRect();
this.mousePos = getLocalPointerPos(ev, rect);
this.cacheMousePos = { ...this.mousePos };
- requestAnimationFrame(() => this.render());
+ this.rafId = requestAnimationFrame(() => this.render());
container.removeEventListener('mousemove', initRender as EventListener);
container.removeEventListener('touchmove', initRender as EventListener);
};
container.addEventListener('mousemove', initRender as EventListener);
container.addEventListener('touchmove', initRender as EventListener);
+ this.handlePointerMove = handlePointerMove;
+ this.initRender = initRender;
}
private render() {
+ if (this.destroyed) return;
+
const distance = getMouseDistance(this.mousePos, this.lastMousePos);
this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.1);
this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.1);
@@ -357,7 +417,7 @@ class ImageTrailVariant3 {
if (this.isIdle && this.zIndexVal !== 1) {
this.zIndexVal = 1;
}
- requestAnimationFrame(() => this.render());
+ this.rafId = requestAnimationFrame(() => this.render());
}
private showNextImage() {
@@ -415,6 +475,22 @@ class ImageTrailVariant3 {
);
}
+ public destroy() {
+ this.destroyed = true;
+ if (this.rafId !== null) {
+ cancelAnimationFrame(this.rafId);
+ this.rafId = null;
+ }
+ this.container.removeEventListener('mousemove', this.handlePointerMove as EventListener);
+ this.container.removeEventListener('touchmove', this.handlePointerMove as EventListener);
+ this.container.removeEventListener('mousemove', this.initRender as EventListener);
+ this.container.removeEventListener('touchmove', this.initRender as EventListener);
+ this.images.forEach(img => {
+ gsap.killTweensOf(img.DOM.el);
+ img.destroy();
+ });
+ }
+
private onImageActivated() {
this.activeImagesCount++;
this.isIdle = false;
@@ -441,6 +517,10 @@ class ImageTrailVariant4 {
private mousePos: { x: number; y: number };
private lastMousePos: { x: number; y: number };
private cacheMousePos: { x: number; y: number };
+ private rafId: number | null = null;
+ private destroyed = false;
+ private handlePointerMove!: (ev: MouseEvent | TouchEvent) => void;
+ private initRender!: (ev: MouseEvent | TouchEvent) => void;
constructor(container: HTMLDivElement) {
this.container = container;
@@ -467,15 +547,19 @@ class ImageTrailVariant4 {
const rect = container.getBoundingClientRect();
this.mousePos = getLocalPointerPos(ev, rect);
this.cacheMousePos = { ...this.mousePos };
- requestAnimationFrame(() => this.render());
+ this.rafId = requestAnimationFrame(() => this.render());
container.removeEventListener('mousemove', initRender as EventListener);
container.removeEventListener('touchmove', initRender as EventListener);
};
container.addEventListener('mousemove', initRender as EventListener);
container.addEventListener('touchmove', initRender as EventListener);
+ this.handlePointerMove = handlePointerMove;
+ this.initRender = initRender;
}
private render() {
+ if (this.destroyed) return;
+
const distance = getMouseDistance(this.mousePos, this.lastMousePos);
if (distance > this.threshold) {
this.showNextImage();
@@ -485,7 +569,7 @@ class ImageTrailVariant4 {
this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.1);
if (this.isIdle && this.zIndexVal !== 1) this.zIndexVal = 1;
- requestAnimationFrame(() => this.render());
+ this.rafId = requestAnimationFrame(() => this.render());
}
private showNextImage() {
@@ -565,6 +649,22 @@ class ImageTrailVariant4 {
);
}
+ public destroy() {
+ this.destroyed = true;
+ if (this.rafId !== null) {
+ cancelAnimationFrame(this.rafId);
+ this.rafId = null;
+ }
+ this.container.removeEventListener('mousemove', this.handlePointerMove as EventListener);
+ this.container.removeEventListener('touchmove', this.handlePointerMove as EventListener);
+ this.container.removeEventListener('mousemove', this.initRender as EventListener);
+ this.container.removeEventListener('touchmove', this.initRender as EventListener);
+ this.images.forEach(img => {
+ gsap.killTweensOf(img.DOM.el);
+ img.destroy();
+ });
+ }
+
private onImageActivated() {
this.activeImagesCount++;
this.isIdle = false;
@@ -591,6 +691,10 @@ class ImageTrailVariant5 {
private mousePos: { x: number; y: number };
private lastMousePos: { x: number; y: number };
private cacheMousePos: { x: number; y: number };
+ private rafId: number | null = null;
+ private destroyed = false;
+ private handlePointerMove!: (ev: MouseEvent | TouchEvent) => void;
+ private initRender!: (ev: MouseEvent | TouchEvent) => void;
private lastAngle: number;
constructor(container: HTMLDivElement) {
@@ -619,15 +723,19 @@ class ImageTrailVariant5 {
const rect = container.getBoundingClientRect();
this.mousePos = getLocalPointerPos(ev, rect);
this.cacheMousePos = { ...this.mousePos };
- requestAnimationFrame(() => this.render());
+ this.rafId = requestAnimationFrame(() => this.render());
container.removeEventListener('mousemove', initRender as EventListener);
container.removeEventListener('touchmove', initRender as EventListener);
};
container.addEventListener('mousemove', initRender as EventListener);
container.addEventListener('touchmove', initRender as EventListener);
+ this.handlePointerMove = handlePointerMove;
+ this.initRender = initRender;
}
private render() {
+ if (this.destroyed) return;
+
const distance = getMouseDistance(this.mousePos, this.lastMousePos);
if (distance > this.threshold) {
this.showNextImage();
@@ -636,7 +744,7 @@ class ImageTrailVariant5 {
this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.1);
this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.1);
if (this.isIdle && this.zIndexVal !== 1) this.zIndexVal = 1;
- requestAnimationFrame(() => this.render());
+ this.rafId = requestAnimationFrame(() => this.render());
}
private showNextImage() {
@@ -709,6 +817,22 @@ class ImageTrailVariant5 {
);
}
+ public destroy() {
+ this.destroyed = true;
+ if (this.rafId !== null) {
+ cancelAnimationFrame(this.rafId);
+ this.rafId = null;
+ }
+ this.container.removeEventListener('mousemove', this.handlePointerMove as EventListener);
+ this.container.removeEventListener('touchmove', this.handlePointerMove as EventListener);
+ this.container.removeEventListener('mousemove', this.initRender as EventListener);
+ this.container.removeEventListener('touchmove', this.initRender as EventListener);
+ this.images.forEach(img => {
+ gsap.killTweensOf(img.DOM.el);
+ img.destroy();
+ });
+ }
+
private onImageActivated() {
this.activeImagesCount++;
this.isIdle = false;
@@ -733,6 +857,10 @@ class ImageTrailVariant6 {
private mousePos: { x: number; y: number };
private lastMousePos: { x: number; y: number };
private cacheMousePos: { x: number; y: number };
+ private rafId: number | null = null;
+ private destroyed = false;
+ private handlePointerMove!: (ev: MouseEvent | TouchEvent) => void;
+ private initRender!: (ev: MouseEvent | TouchEvent) => void;
constructor(container: HTMLDivElement) {
this.container = container;
@@ -759,15 +887,19 @@ class ImageTrailVariant6 {
const rect = container.getBoundingClientRect();
this.mousePos = getLocalPointerPos(ev, rect);
this.cacheMousePos = { ...this.mousePos };
- requestAnimationFrame(() => this.render());
+ this.rafId = requestAnimationFrame(() => this.render());
container.removeEventListener('mousemove', initRender as EventListener);
container.removeEventListener('touchmove', initRender as EventListener);
};
container.addEventListener('mousemove', initRender as EventListener);
container.addEventListener('touchmove', initRender as EventListener);
+ this.handlePointerMove = handlePointerMove;
+ this.initRender = initRender;
}
private render() {
+ if (this.destroyed) return;
+
const distance = getMouseDistance(this.mousePos, this.lastMousePos);
this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.3);
this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.3);
@@ -779,7 +911,7 @@ class ImageTrailVariant6 {
if (this.isIdle && this.zIndexVal !== 1) {
this.zIndexVal = 1;
}
- requestAnimationFrame(() => this.render());
+ this.rafId = requestAnimationFrame(() => this.render());
}
private mapSpeedToSize(speed: number, minSize: number, maxSize: number) {
@@ -863,6 +995,22 @@ class ImageTrailVariant6 {
);
}
+ public destroy() {
+ this.destroyed = true;
+ if (this.rafId !== null) {
+ cancelAnimationFrame(this.rafId);
+ this.rafId = null;
+ }
+ this.container.removeEventListener('mousemove', this.handlePointerMove as EventListener);
+ this.container.removeEventListener('touchmove', this.handlePointerMove as EventListener);
+ this.container.removeEventListener('mousemove', this.initRender as EventListener);
+ this.container.removeEventListener('touchmove', this.initRender as EventListener);
+ this.images.forEach(img => {
+ gsap.killTweensOf(img.DOM.el);
+ img.destroy();
+ });
+ }
+
private onImageActivated() {
this.activeImagesCount++;
this.isIdle = false;
@@ -898,6 +1046,10 @@ class ImageTrailVariant7 {
private mousePos: { x: number; y: number };
private lastMousePos: { x: number; y: number };
private cacheMousePos: { x: number; y: number };
+ private rafId: number | null = null;
+ private destroyed = false;
+ private handlePointerMove!: (ev: MouseEvent | TouchEvent) => void;
+ private initRender!: (ev: MouseEvent | TouchEvent) => void;
private visibleImagesCount: number;
private visibleImagesTotal: number;
@@ -929,15 +1081,19 @@ class ImageTrailVariant7 {
const rect = container.getBoundingClientRect();
this.mousePos = getLocalPointerPos(ev, rect);
this.cacheMousePos = { ...this.mousePos };
- requestAnimationFrame(() => this.render());
+ this.rafId = requestAnimationFrame(() => this.render());
container.removeEventListener('mousemove', initRender as EventListener);
container.removeEventListener('touchmove', initRender as EventListener);
};
container.addEventListener('mousemove', initRender as EventListener);
container.addEventListener('touchmove', initRender as EventListener);
+ this.handlePointerMove = handlePointerMove;
+ this.initRender = initRender;
}
private render() {
+ if (this.destroyed) return;
+
const distance = getMouseDistance(this.mousePos, this.lastMousePos);
this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.3);
this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.3);
@@ -948,7 +1104,7 @@ class ImageTrailVariant7 {
}
if (this.isIdle && this.zIndexVal !== 1) this.zIndexVal = 1;
- requestAnimationFrame(() => this.render());
+ this.rafId = requestAnimationFrame(() => this.render());
}
private showNextImage() {
@@ -1003,6 +1159,22 @@ class ImageTrailVariant7 {
}
}
+ public destroy() {
+ this.destroyed = true;
+ if (this.rafId !== null) {
+ cancelAnimationFrame(this.rafId);
+ this.rafId = null;
+ }
+ this.container.removeEventListener('mousemove', this.handlePointerMove as EventListener);
+ this.container.removeEventListener('touchmove', this.handlePointerMove as EventListener);
+ this.container.removeEventListener('mousemove', this.initRender as EventListener);
+ this.container.removeEventListener('touchmove', this.initRender as EventListener);
+ this.images.forEach(img => {
+ gsap.killTweensOf(img.DOM.el);
+ img.destroy();
+ });
+ }
+
private onImageActivated() {
this.activeImagesCount++;
this.isIdle = false;
@@ -1026,6 +1198,10 @@ class ImageTrailVariant8 {
private mousePos: { x: number; y: number };
private lastMousePos: { x: number; y: number };
private cacheMousePos: { x: number; y: number };
+ private rafId: number | null = null;
+ private destroyed = false;
+ private handlePointerMove!: (ev: MouseEvent | TouchEvent) => void;
+ private initRender!: (ev: MouseEvent | TouchEvent) => void;
private rotation: { x: number; y: number };
private cachedRotation: { x: number; y: number };
private zValue: number;
@@ -1060,15 +1236,19 @@ class ImageTrailVariant8 {
const rect = container.getBoundingClientRect();
this.mousePos = getLocalPointerPos(ev, rect);
this.cacheMousePos = { ...this.mousePos };
- requestAnimationFrame(() => this.render());
+ this.rafId = requestAnimationFrame(() => this.render());
container.removeEventListener('mousemove', initRender as EventListener);
container.removeEventListener('touchmove', initRender as EventListener);
};
container.addEventListener('mousemove', initRender as EventListener);
container.addEventListener('touchmove', initRender as EventListener);
+ this.handlePointerMove = handlePointerMove;
+ this.initRender = initRender;
}
private render() {
+ if (this.destroyed) return;
+
const distance = getMouseDistance(this.mousePos, this.lastMousePos);
this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.1);
this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.1);
@@ -1080,7 +1260,7 @@ class ImageTrailVariant8 {
if (this.isIdle && this.zIndexVal !== 1) {
this.zIndexVal = 1;
}
- requestAnimationFrame(() => this.render());
+ this.rafId = requestAnimationFrame(() => this.render());
}
private showNextImage() {
@@ -1149,6 +1329,22 @@ class ImageTrailVariant8 {
);
}
+ public destroy() {
+ this.destroyed = true;
+ if (this.rafId !== null) {
+ cancelAnimationFrame(this.rafId);
+ this.rafId = null;
+ }
+ this.container.removeEventListener('mousemove', this.handlePointerMove as EventListener);
+ this.container.removeEventListener('touchmove', this.handlePointerMove as EventListener);
+ this.container.removeEventListener('mousemove', this.initRender as EventListener);
+ this.container.removeEventListener('touchmove', this.initRender as EventListener);
+ this.images.forEach(img => {
+ gsap.killTweensOf(img.DOM.el);
+ img.destroy();
+ });
+ }
+
private onImageActivated() {
this.activeImagesCount++;
this.isIdle = false;
@@ -1194,7 +1390,11 @@ export default function ImageTrail({ items = [], variant = 1 }: ImageTrailProps)
useEffect(() => {
if (!containerRef.current) return;
const Cls = variantMap[variant] || variantMap[1];
- new Cls(containerRef.current);
+ const instance = new Cls(containerRef.current);
+
+ return () => {
+ instance.destroy();
+ };
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [variant, items]);