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 );\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 );\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 );\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 );\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 );\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 );\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 );\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 );\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 {items.map((label, index) => (\n {\n itemRefs.current[index] = el;\n }}\n className=\"line-sidebar__item\"\n aria-current={activeIndex === index ? 'true' : undefined}\n onClick={() => handleClick(index, label)}\n >\n {showMarker && }\n \n {showIndex && {String(index + 1).padStart(2, '0')}}\n {label}\n \n \n ))}\n
\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 {items.map((label, index) => (\n {\n itemRefs.current[index] = el;\n }}\n className=\"line-sidebar__item\"\n aria-current={activeIndex === index ? 'true' : undefined}\n onClick={() => handleClick(index, label)}\n >\n {showMarker && }\n \n {showIndex && {String(index + 1).padStart(2, '0')}}\n {label}\n \n \n ))}\n
\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 {items.map((label, index) => (\n {\n itemRefs.current[index] = el;\n }}\n aria-current={activeIndex === index ? 'true' : undefined}\n onClick={() => handleClick(index, label)}\n className={`relative cursor-pointer before:absolute before:-inset-x-12 before:-inset-y-[6px] before:content-[''] ${tickClass}`}\n >\n {showMarker && (\n \n )}\n \n {showIndex && (\n \n {String(index + 1).padStart(2, '0')}\n \n )}\n {label}\n \n \n ))}\n \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 {items.map((label, index) => (\n {\n itemRefs.current[index] = el;\n }}\n aria-current={activeIndex === index ? 'true' : undefined}\n onClick={() => handleClick(index, label)}\n className={`relative cursor-pointer before:absolute before:-inset-x-12 before:-inset-y-[6px] before:content-[''] ${tickClass}`}\n >\n {showMarker && (\n \n )}\n \n {showIndex && (\n \n {String(index + 1).padStart(2, '0')}\n \n )}\n {label}\n \n \n ))}\n \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 {items.map((label, index) => (\n {\n itemRefs.current[index] = el;\n }}\n className=\"line-sidebar__item\"\n aria-current={activeIndex === index ? 'true' : undefined}\n onClick={() => handleClick(index, label)}\n >\n {showMarker && }\n \n {showIndex && {String(index + 1).padStart(2, '0')}}\n {label}\n \n \n ))}\n
\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 {items.map((label, index) => (\n {\n itemRefs.current[index] = el;\n }}\n className=\"line-sidebar__item\"\n aria-current={activeIndex === index ? 'true' : undefined}\n onClick={() => handleClick(index, label)}\n >\n {showMarker && }\n \n {showIndex && {String(index + 1).padStart(2, '0')}}\n {label}\n \n \n ))}\n
\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 {items.map((label, index) => (\n {\n itemRefs.current[index] = el;\n }}\n aria-current={activeIndex === index ? 'true' : undefined}\n onClick={() => handleClick(index, label)}\n className={`relative cursor-pointer before:absolute before:-inset-x-12 before:-inset-y-[6px] before:content-[''] ${tickClass}`}\n >\n {showMarker && (\n \n )}\n \n {showIndex && (\n \n {String(index + 1).padStart(2, '0')}\n \n )}\n {label}\n \n \n ))}\n \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 {items.map((label, index) => (\n {\n itemRefs.current[index] = el;\n }}\n aria-current={activeIndex === index ? 'true' : undefined}\n onClick={() => handleClick(index, label)}\n className={`relative cursor-pointer before:absolute before:-inset-x-12 before:-inset-y-[6px] before:content-[''] ${tickClass}`}\n >\n {showMarker && (\n \n )}\n \n {showIndex && (\n \n {String(index + 1).padStart(2, '0')}\n \n )}\n {label}\n \n \n ))}\n \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]);