From 7cc457ecfe6e3f386e2b7f461e2ee01f90af9bb0 Mon Sep 17 00:00:00 2001 From: Lin Junrong Date: Tue, 4 Aug 2026 13:59:20 +0800 Subject: [PATCH] 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]);