From 79acd46355c610771b74353d4dfa5b6bd8c8f964 Mon Sep 17 00:00:00 2001 From: Lin Junrong Date: Tue, 4 Aug 2026 13:59:20 +0800 Subject: [PATCH 1/3] fix(ImageTrail): stop the render loop and drop listeners on unmount The effect instantiates a variant class and returns nothing, so nothing is ever torn down. Each of the eight variants starts a self-scheduling `requestAnimationFrame(() => this.render())` loop and every `ImageItem` registers a `resize` handler on `window`. Both outlive the component. The effect list is `[variant, items]`, and `items` is an array prop, so a parent that renders an inline array gets a fresh reference on every render. That re-runs the effect and constructs another instance, each one adding its own permanent rAF loop, its own window listener and its own pair of container listeners. Nothing releases the previous instance, so the cost is cumulative rather than a single stranded loop. `ImageItem` now exposes `destroy()` to unregister its resize handler, the variants keep the frame id and the two container handlers so they can be released, `render()` returns early once destroyed, and `destroy()` cancels the pending frame, removes the listeners, kills the GSAP tweens still targeting the images and disposes each item. The effect returns a cleanup that calls it. Applied to all four variants of the component (JS/TS x CSS/Tailwind). --- .../Animations/ImageTrail/ImageTrail.jsx | 218 ++++++++++++++-- .../Animations/ImageTrail/ImageTrail.jsx | 218 ++++++++++++++-- .../Animations/ImageTrail/ImageTrail.tsx | 234 ++++++++++++++++-- .../Animations/ImageTrail/ImageTrail.tsx | 234 ++++++++++++++++-- 4 files changed, 836 insertions(+), 68 deletions(-) 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]); From 9eba4dacddcbdaa053d8207d23f01867ec5007c8 Mon Sep 17 00:00:00 2001 From: Lin Junrong Date: Tue, 4 Aug 2026 13:59:20 +0800 Subject: [PATCH 2/3] chore(registry): rebuild ImageTrail artifacts public/r/*.json embeds the component source verbatim and is what the jsrepo CLI writes into a user's project, so a source-only fix would still ship the leaking version. --- public/r/ImageTrail-JS-CSS.json | 2 +- public/r/ImageTrail-JS-TW.json | 2 +- public/r/ImageTrail-TS-CSS.json | 2 +- public/r/ImageTrail-TS-TW.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) 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": [], From 5588d8a795b54797eaaeef1f4a128eb70b496694 Mon Sep 17 00:00:00 2001 From: Lin Junrong Date: Tue, 4 Aug 2026 13:59:20 +0800 Subject: [PATCH 3/3] fix(registry): rebuild stale LineSidebar and OptionWheel artifacts `src/` carries the StrictMode rAF fix (cleanup nulls `rafRef.current` so the loop can restart), but the published registry was never regenerated after it landed. The shipped payload still has the pre-fix shape: if (rafRef.current != null) return; // old, in public/r/*.json ... // and no `rafRef.current = null` // in the cleanup So anyone installing LineSidebar or OptionWheel through the CLI still receives the version whose animation never restarts after a StrictMode remount. Regenerated with `npm run registry:build`; no source change. --- public/r/LineSidebar-JS-CSS.json | 2 +- public/r/LineSidebar-JS-TW.json | 2 +- public/r/LineSidebar-TS-CSS.json | 2 +- public/r/LineSidebar-TS-TW.json | 2 +- public/r/OptionWheel-JS-CSS.json | 2 +- public/r/OptionWheel-JS-TW.json | 2 +- public/r/OptionWheel-TS-CSS.json | 2 +- public/r/OptionWheel-TS-TW.json | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) 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": [],