import { useRef, useEffect } from 'react';
import { gsap } from 'gsap';
function lerp(a: number, b: number, n: number): number {
return (1 - n) * a + n * b;
}
function getLocalPointerPos(e: MouseEvent | TouchEvent, rect: DOMRect): { x: number; y: number } {
let clientX = 0,
clientY = 0;
if ('touches' in e && e.touches.length > 0) {
clientX = e.touches[0].clientX;
clientY = e.touches[0].clientY;
} else if ('clientX' in e) {
clientX = e.clientX;
clientY = e.clientY;
}
return {
x: clientX - rect.left,
y: clientY - rect.top
};
}
function getMouseDistance(p1: { x: number; y: number }, p2: { x: number; y: number }): number {
const dx = p1.x - p2.x;
const dy = p1.y - p2.y;
return Math.hypot(dx, dy);
}
class ImageItem {
public DOM: { el: HTMLDivElement; inner: HTMLDivElement | null } = {
el: null as unknown as HTMLDivElement,
inner: null
};
public defaultStyle: gsap.TweenVars = { scale: 1, x: 0, y: 0, opacity: 0 };
public rect: DOMRect | null = null;
private resize!: () => void;
constructor(DOM_el: HTMLDivElement) {
this.DOM.el = DOM_el;
this.DOM.inner = this.DOM.el.querySelector('.content__img-inner');
this.getRect();
this.initEvents();
}
private initEvents() {
this.resize = () => {
gsap.set(this.DOM.el, this.defaultStyle);
this.getRect();
};
window.addEventListener('resize', this.resize);
}
private getRect() {
this.rect = this.DOM.el.getBoundingClientRect();
}
}
class ImageTrailVariant1 {
private container: HTMLDivElement;
private DOM: { el: HTMLDivElement };
private images: ImageItem[];
private imagesTotal: number;
private imgPosition: number;
private zIndexVal: number;
private activeImagesCount: number;
private isIdle: boolean;
private threshold: number;
private mousePos: { x: number; y: number };
private lastMousePos: { x: number; y: number };
private cacheMousePos: { x: number; y: number };
constructor(container: HTMLDivElement) {
this.container = container;
this.DOM = { el: container };
this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img as HTMLDivElement));
this.imagesTotal = this.images.length;
this.imgPosition = 0;
this.zIndexVal = 1;
this.activeImagesCount = 0;
this.isIdle = true;
this.threshold = 80;
this.mousePos = { x: 0, y: 0 };
this.lastMousePos = { x: 0, y: 0 };
this.cacheMousePos = { x: 0, y: 0 };
const handlePointerMove = (ev: MouseEvent | TouchEvent) => {
const rect = this.container.getBoundingClientRect();
this.mousePos = getLocalPointerPos(ev, rect);
};
container.addEventListener('mousemove', handlePointerMove);
container.addEventListener('touchmove', handlePointerMove);
const initRender = (ev: MouseEvent | TouchEvent) => {
const rect = this.container.getBoundingClientRect();
this.mousePos = getLocalPointerPos(ev, rect);
this.cacheMousePos = { ...this.mousePos };
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);
}
private render() {
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);
if (distance > this.threshold) {
this.showNextImage();
this.lastMousePos = { ...this.mousePos };
}
if (this.isIdle && this.zIndexVal !== 1) {
this.zIndexVal = 1;
}
requestAnimationFrame(() => this.render());
}
private showNextImage() {
++this.zIndexVal;
this.imgPosition = this.imgPosition < this.imagesTotal - 1 ? this.imgPosition + 1 : 0;
const img = this.images[this.imgPosition];
gsap.killTweensOf(img.DOM.el);
gsap
.timeline({
onStart: () => this.onImageActivated(),
onComplete: () => this.onImageDeactivated()
})
.fromTo(
img.DOM.el,
{
opacity: 1,
scale: 1,
zIndex: this.zIndexVal,
x: this.cacheMousePos.x - (img.rect?.width ?? 0) / 2,
y: this.cacheMousePos.y - (img.rect?.height ?? 0) / 2
},
{
duration: 0.4,
ease: 'power1',
x: this.mousePos.x - (img.rect?.width ?? 0) / 2,
y: this.mousePos.y - (img.rect?.height ?? 0) / 2
},
0
)
.to(
img.DOM.el,
{
duration: 0.4,
ease: 'power3',
opacity: 0,
scale: 0.2
},
0.4
);
}
private onImageActivated() {
this.activeImagesCount++;
this.isIdle = false;
}
private onImageDeactivated() {
this.activeImagesCount--;
if (this.activeImagesCount === 0) {
this.isIdle = true;
}
}
}
class ImageTrailVariant2 {
private container: HTMLDivElement;
private DOM: { el: HTMLDivElement };
private images: ImageItem[];
private imagesTotal: number;
private imgPosition: number;
private zIndexVal: number;
private activeImagesCount: number;
private isIdle: boolean;
private threshold: number;
private mousePos: { x: number; y: number };
private lastMousePos: { x: number; y: number };
private cacheMousePos: { x: number; y: number };
constructor(container: HTMLDivElement) {
this.container = container;
this.DOM = { el: container };
this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img as HTMLDivElement));
this.imagesTotal = this.images.length;
this.imgPosition = 0;
this.zIndexVal = 1;
this.activeImagesCount = 0;
this.isIdle = true;
this.threshold = 80;
this.mousePos = { x: 0, y: 0 };
this.lastMousePos = { x: 0, y: 0 };
this.cacheMousePos = { x: 0, y: 0 };
const handlePointerMove = (ev: MouseEvent | TouchEvent) => {
const rect = container.getBoundingClientRect();
this.mousePos = getLocalPointerPos(ev, rect);
};
container.addEventListener('mousemove', handlePointerMove);
container.addEventListener('touchmove', handlePointerMove);
const initRender = (ev: MouseEvent | TouchEvent) => {
const rect = container.getBoundingClientRect();
this.mousePos = getLocalPointerPos(ev, rect);
this.cacheMousePos = { ...this.mousePos };
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);
}
private render() {
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);
if (distance > this.threshold) {
this.showNextImage();
this.lastMousePos = { ...this.mousePos };
}
if (this.isIdle && this.zIndexVal !== 1) {
this.zIndexVal = 1;
}
requestAnimationFrame(() => this.render());
}
private showNextImage() {
++this.zIndexVal;
this.imgPosition = this.imgPosition < this.imagesTotal - 1 ? this.imgPosition + 1 : 0;
const img = this.images[this.imgPosition];
gsap.killTweensOf(img.DOM.el);
gsap
.timeline({
onStart: () => this.onImageActivated(),
onComplete: () => this.onImageDeactivated()
})
.fromTo(
img.DOM.el,
{
opacity: 1,
scale: 0,
zIndex: this.zIndexVal,
x: this.cacheMousePos.x - (img.rect?.width ?? 0) / 2,
y: this.cacheMousePos.y - (img.rect?.height ?? 0) / 2
},
{
duration: 0.4,
ease: 'power1',
scale: 1,
x: this.mousePos.x - (img.rect?.width ?? 0) / 2,
y: this.mousePos.y - (img.rect?.height ?? 0) / 2
},
0
)
.fromTo(
img.DOM.inner,
{ scale: 2.8, filter: 'brightness(250%)' },
{
duration: 0.4,
ease: 'power1',
scale: 1,
filter: 'brightness(100%)'
},
0
)
.to(
img.DOM.el,
{
duration: 0.4,
ease: 'power2',
opacity: 0,
scale: 0.2
},
0.45
);
}
private onImageActivated() {
this.activeImagesCount++;
this.isIdle = false;
}
private onImageDeactivated() {
this.activeImagesCount--;
if (this.activeImagesCount === 0) {
this.isIdle = true;
}
}
}
class ImageTrailVariant3 {
private container: HTMLDivElement;
private DOM: { el: HTMLDivElement };
private images: ImageItem[];
private imagesTotal: number;
private imgPosition: number;
private zIndexVal: number;
private activeImagesCount: number;
private isIdle: boolean;
private threshold: number;
private mousePos: { x: number; y: number };
private lastMousePos: { x: number; y: number };
private cacheMousePos: { x: number; y: number };
constructor(container: HTMLDivElement) {
this.container = container;
this.DOM = { el: container };
this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img as HTMLDivElement));
this.imagesTotal = this.images.length;
this.imgPosition = 0;
this.zIndexVal = 1;
this.activeImagesCount = 0;
this.isIdle = true;
this.threshold = 80;
this.mousePos = { x: 0, y: 0 };
this.lastMousePos = { x: 0, y: 0 };
this.cacheMousePos = { x: 0, y: 0 };
const handlePointerMove = (ev: MouseEvent | TouchEvent) => {
const rect = container.getBoundingClientRect();
this.mousePos = getLocalPointerPos(ev, rect);
};
container.addEventListener('mousemove', handlePointerMove);
container.addEventListener('touchmove', handlePointerMove);
const initRender = (ev: MouseEvent | TouchEvent) => {
const rect = container.getBoundingClientRect();
this.mousePos = getLocalPointerPos(ev, rect);
this.cacheMousePos = { ...this.mousePos };
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);
}
private render() {
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);
if (distance > this.threshold) {
this.showNextImage();
this.lastMousePos = { ...this.mousePos };
}
if (this.isIdle && this.zIndexVal !== 1) {
this.zIndexVal = 1;
}
requestAnimationFrame(() => this.render());
}
private showNextImage() {
++this.zIndexVal;
this.imgPosition = this.imgPosition < this.imagesTotal - 1 ? this.imgPosition + 1 : 0;
const img = this.images[this.imgPosition];
gsap.killTweensOf(img.DOM.el);
gsap
.timeline({
onStart: () => this.onImageActivated(),
onComplete: () => this.onImageDeactivated()
})
.fromTo(
img.DOM.el,
{
opacity: 1,
scale: 0,
zIndex: this.zIndexVal,
xPercent: 0,
yPercent: 0,
x: this.cacheMousePos.x - (img.rect?.width ?? 0) / 2,
y: this.cacheMousePos.y - (img.rect?.height ?? 0) / 2
},
{
duration: 0.4,
ease: 'power1',
scale: 1,
x: this.mousePos.x - (img.rect?.width ?? 0) / 2,
y: this.mousePos.y - (img.rect?.height ?? 0) / 2
},
0
)
.fromTo(
img.DOM.inner,
{ scale: 1.2 },
{
duration: 0.4,
ease: 'power1',
scale: 1
},
0
)
.to(
img.DOM.el,
{
duration: 0.6,
ease: 'power2',
opacity: 0,
scale: 0.2,
xPercent: () => gsap.utils.random(-30, 30),
yPercent: -200
},
0.6
);
}
private onImageActivated() {
this.activeImagesCount++;
this.isIdle = false;
}
private onImageDeactivated() {
this.activeImagesCount--;
if (this.activeImagesCount === 0) {
this.isIdle = true;
}
}
}
class ImageTrailVariant4 {
private container: HTMLDivElement;
private DOM: { el: HTMLDivElement };
private images: ImageItem[];
private imagesTotal: number;
private imgPosition: number;
private zIndexVal: number;
private activeImagesCount: number;
private isIdle: boolean;
private threshold: number;
private mousePos: { x: number; y: number };
private lastMousePos: { x: number; y: number };
private cacheMousePos: { x: number; y: number };
constructor(container: HTMLDivElement) {
this.container = container;
this.DOM = { el: container };
this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img as HTMLDivElement));
this.imagesTotal = this.images.length;
this.imgPosition = 0;
this.zIndexVal = 1;
this.activeImagesCount = 0;
this.isIdle = true;
this.threshold = 80;
this.mousePos = { x: 0, y: 0 };
this.lastMousePos = { x: 0, y: 0 };
this.cacheMousePos = { x: 0, y: 0 };
const handlePointerMove = (ev: MouseEvent | TouchEvent) => {
const rect = container.getBoundingClientRect();
this.mousePos = getLocalPointerPos(ev, rect);
};
container.addEventListener('mousemove', handlePointerMove);
container.addEventListener('touchmove', handlePointerMove);
const initRender = (ev: MouseEvent | TouchEvent) => {
const rect = container.getBoundingClientRect();
this.mousePos = getLocalPointerPos(ev, rect);
this.cacheMousePos = { ...this.mousePos };
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);
}
private render() {
const distance = getMouseDistance(this.mousePos, this.lastMousePos);
if (distance > this.threshold) {
this.showNextImage();
this.lastMousePos = { ...this.mousePos };
}
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());
}
private showNextImage() {
++this.zIndexVal;
this.imgPosition = this.imgPosition < this.imagesTotal - 1 ? this.imgPosition + 1 : 0;
const img = this.images[this.imgPosition];
gsap.killTweensOf(img.DOM.el);
let dx = this.mousePos.x - this.cacheMousePos.x;
let dy = this.mousePos.y - this.cacheMousePos.y;
let distance = Math.sqrt(dx * dx + dy * dy);
if (distance !== 0) {
dx /= distance;
dy /= distance;
}
dx *= distance / 100;
dy *= distance / 100;
gsap
.timeline({
onStart: () => this.onImageActivated(),
onComplete: () => this.onImageDeactivated()
})
.fromTo(
img.DOM.el,
{
opacity: 1,
scale: 0,
zIndex: this.zIndexVal,
x: this.cacheMousePos.x - (img.rect?.width ?? 0) / 2,
y: this.cacheMousePos.y - (img.rect?.height ?? 0) / 2
},
{
duration: 0.4,
ease: 'power1',
scale: 1,
x: this.mousePos.x - (img.rect?.width ?? 0) / 2,
y: this.mousePos.y - (img.rect?.height ?? 0) / 2
},
0
)
.fromTo(
img.DOM.inner,
{
scale: 2,
filter: `brightness(${Math.max((400 * distance) / 100, 100)}%) contrast(${Math.max(
(400 * distance) / 100,
100
)}%)`
},
{
duration: 0.4,
ease: 'power1',
scale: 1,
filter: 'brightness(100%) contrast(100%)'
},
0
)
.to(
img.DOM.el,
{
duration: 0.4,
ease: 'power3',
opacity: 0
},
0.4
)
.to(
img.DOM.el,
{
duration: 1.5,
ease: 'power4',
x: `+=${dx * 110}`,
y: `+=${dy * 110}`
},
0.05
);
}
private onImageActivated() {
this.activeImagesCount++;
this.isIdle = false;
}
private onImageDeactivated() {
this.activeImagesCount--;
if (this.activeImagesCount === 0) {
this.isIdle = true;
}
}
}
class ImageTrailVariant5 {
private container: HTMLDivElement;
private DOM: { el: HTMLDivElement };
private images: ImageItem[];
private imagesTotal: number;
private imgPosition: number;
private zIndexVal: number;
private activeImagesCount: number;
private isIdle: boolean;
private threshold: number;
private mousePos: { x: number; y: number };
private lastMousePos: { x: number; y: number };
private cacheMousePos: { x: number; y: number };
private lastAngle: number;
constructor(container: HTMLDivElement) {
this.container = container;
this.DOM = { el: container };
this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img as HTMLDivElement));
this.imagesTotal = this.images.length;
this.imgPosition = 0;
this.zIndexVal = 1;
this.activeImagesCount = 0;
this.isIdle = true;
this.threshold = 80;
this.mousePos = { x: 0, y: 0 };
this.lastMousePos = { x: 0, y: 0 };
this.cacheMousePos = { x: 0, y: 0 };
this.lastAngle = 0;
const handlePointerMove = (ev: MouseEvent | TouchEvent) => {
const rect = container.getBoundingClientRect();
this.mousePos = getLocalPointerPos(ev, rect);
};
container.addEventListener('mousemove', handlePointerMove);
container.addEventListener('touchmove', handlePointerMove);
const initRender = (ev: MouseEvent | TouchEvent) => {
const rect = container.getBoundingClientRect();
this.mousePos = getLocalPointerPos(ev, rect);
this.cacheMousePos = { ...this.mousePos };
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);
}
private render() {
const distance = getMouseDistance(this.mousePos, this.lastMousePos);
if (distance > this.threshold) {
this.showNextImage();
this.lastMousePos = { ...this.mousePos };
}
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());
}
private showNextImage() {
let dx = this.mousePos.x - this.cacheMousePos.x;
let dy = this.mousePos.y - this.cacheMousePos.y;
let angle = Math.atan2(dy, dx) * (180 / Math.PI);
if (angle < 0) angle += 360;
if (angle > 90 && angle <= 270) angle += 180;
const isMovingClockwise = angle >= this.lastAngle;
this.lastAngle = angle;
let startAngle = isMovingClockwise ? angle - 10 : angle + 10;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance !== 0) {
dx /= distance;
dy /= distance;
}
dx *= distance / 150;
dy *= distance / 150;
++this.zIndexVal;
this.imgPosition = this.imgPosition < this.imagesTotal - 1 ? this.imgPosition + 1 : 0;
const img = this.images[this.imgPosition];
gsap.killTweensOf(img.DOM.el);
gsap
.timeline({
onStart: () => this.onImageActivated(),
onComplete: () => this.onImageDeactivated()
})
.fromTo(
img.DOM.el,
{
opacity: 1,
filter: 'brightness(80%)',
scale: 0.1,
zIndex: this.zIndexVal,
x: this.cacheMousePos.x - (img.rect?.width ?? 0) / 2,
y: this.cacheMousePos.y - (img.rect?.height ?? 0) / 2,
rotation: startAngle
},
{
duration: 1,
ease: 'power2',
scale: 1,
filter: 'brightness(100%)',
x: this.mousePos.x - (img.rect?.width ?? 0) / 2 + dx * 70,
y: this.mousePos.y - (img.rect?.height ?? 0) / 2 + dy * 70,
rotation: this.lastAngle
},
0
)
.to(
img.DOM.el,
{
duration: 0.4,
ease: 'expo',
opacity: 0
},
0.5
)
.to(
img.DOM.el,
{
duration: 1.5,
ease: 'power4',
x: `+=${dx * 120}`,
y: `+=${dy * 120}`
},
0.05
);
}
private onImageActivated() {
this.activeImagesCount++;
this.isIdle = false;
}
private onImageDeactivated() {
this.activeImagesCount--;
if (this.activeImagesCount === 0) this.isIdle = true;
}
}
class ImageTrailVariant6 {
private container: HTMLDivElement;
private DOM: { el: HTMLDivElement };
private images: ImageItem[];
private imagesTotal: number;
private imgPosition: number;
private zIndexVal: number;
private activeImagesCount: number;
private isIdle: boolean;
private threshold: number;
private mousePos: { x: number; y: number };
private lastMousePos: { x: number; y: number };
private cacheMousePos: { x: number; y: number };
constructor(container: HTMLDivElement) {
this.container = container;
this.DOM = { el: container };
this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img as HTMLDivElement));
this.imagesTotal = this.images.length;
this.imgPosition = 0;
this.zIndexVal = 1;
this.activeImagesCount = 0;
this.isIdle = true;
this.threshold = 80;
this.mousePos = { x: 0, y: 0 };
this.lastMousePos = { x: 0, y: 0 };
this.cacheMousePos = { x: 0, y: 0 };
const handlePointerMove = (ev: MouseEvent | TouchEvent) => {
const rect = container.getBoundingClientRect();
this.mousePos = getLocalPointerPos(ev, rect);
};
container.addEventListener('mousemove', handlePointerMove);
container.addEventListener('touchmove', handlePointerMove);
const initRender = (ev: MouseEvent | TouchEvent) => {
const rect = container.getBoundingClientRect();
this.mousePos = getLocalPointerPos(ev, rect);
this.cacheMousePos = { ...this.mousePos };
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);
}
private render() {
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);
if (distance > this.threshold) {
this.showNextImage();
this.lastMousePos = { ...this.mousePos };
}
if (this.isIdle && this.zIndexVal !== 1) {
this.zIndexVal = 1;
}
requestAnimationFrame(() => this.render());
}
private mapSpeedToSize(speed: number, minSize: number, maxSize: number) {
const maxSpeed = 200;
return minSize + (maxSize - minSize) * Math.min(speed / maxSpeed, 1);
}
private mapSpeedToBrightness(speed: number, minB: number, maxB: number) {
const maxSpeed = 70;
return minB + (maxB - minB) * Math.min(speed / maxSpeed, 1);
}
private mapSpeedToBlur(speed: number, minBlur: number, maxBlur: number) {
const maxSpeed = 90;
return minBlur + (maxBlur - minBlur) * Math.min(speed / maxSpeed, 1);
}
private mapSpeedToGrayscale(speed: number, minG: number, maxG: number) {
const maxSpeed = 90;
return minG + (maxG - minG) * Math.min(speed / maxSpeed, 1);
}
private showNextImage() {
const dx = this.mousePos.x - this.cacheMousePos.x;
const dy = this.mousePos.y - this.cacheMousePos.y;
const speed = Math.sqrt(dx * dx + dy * dy);
++this.zIndexVal;
this.imgPosition = this.imgPosition < this.imagesTotal - 1 ? this.imgPosition + 1 : 0;
const img = this.images[this.imgPosition];
const scaleFactor = this.mapSpeedToSize(speed, 0.3, 2);
const brightnessValue = this.mapSpeedToBrightness(speed, 0, 1.3);
const blurValue = this.mapSpeedToBlur(speed, 20, 0);
const grayscaleValue = this.mapSpeedToGrayscale(speed, 600, 0);
gsap.killTweensOf(img.DOM.el);
gsap
.timeline({
onStart: () => this.onImageActivated(),
onComplete: () => this.onImageDeactivated()
})
.fromTo(
img.DOM.el,
{
opacity: 1,
scale: 0,
zIndex: this.zIndexVal,
x: this.cacheMousePos.x - (img.rect?.width ?? 0) / 2,
y: this.cacheMousePos.y - (img.rect?.height ?? 0) / 2
},
{
duration: 0.8,
ease: 'power3',
scale: scaleFactor,
filter: `grayscale(${grayscaleValue * 100}%) brightness(${brightnessValue * 100}%) blur(${blurValue}px)`,
x: this.mousePos.x - (img.rect?.width ?? 0) / 2,
y: this.mousePos.y - (img.rect?.height ?? 0) / 2
},
0
)
.fromTo(
img.DOM.inner,
{ scale: 2 },
{
duration: 0.8,
ease: 'power3',
scale: 1
},
0
)
.to(
img.DOM.el,
{
duration: 0.4,
ease: 'power3.in',
opacity: 0,
scale: 0.2
},
0.45
);
}
private onImageActivated() {
this.activeImagesCount++;
this.isIdle = false;
}
private onImageDeactivated() {
this.activeImagesCount--;
if (this.activeImagesCount === 0) {
this.isIdle = true;
}
}
}
function getNewPosition(position: number, offset: number, arr: ImageItem[]) {
const realOffset = Math.abs(offset) % arr.length;
if (position - realOffset >= 0) {
return position - realOffset;
} else {
return arr.length - (realOffset - position);
}
}
class ImageTrailVariant7 {
private container: HTMLDivElement;
private DOM: { el: HTMLDivElement };
private images: ImageItem[];
private imagesTotal: number;
private imgPosition: number;
private zIndexVal: number;
private activeImagesCount: number;
private isIdle: boolean;
private threshold: number;
private mousePos: { x: number; y: number };
private lastMousePos: { x: number; y: number };
private cacheMousePos: { x: number; y: number };
private visibleImagesCount: number;
private visibleImagesTotal: number;
constructor(container: HTMLDivElement) {
this.container = container;
this.DOM = { el: container };
this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img as HTMLDivElement));
this.imagesTotal = this.images.length;
this.imgPosition = 0;
this.zIndexVal = 1;
this.activeImagesCount = 0;
this.isIdle = true;
this.threshold = 80;
this.mousePos = { x: 0, y: 0 };
this.lastMousePos = { x: 0, y: 0 };
this.cacheMousePos = { x: 0, y: 0 };
this.visibleImagesCount = 0;
this.visibleImagesTotal = 9;
this.visibleImagesTotal = Math.min(this.visibleImagesTotal, this.imagesTotal - 1);
const handlePointerMove = (ev: MouseEvent | TouchEvent) => {
const rect = container.getBoundingClientRect();
this.mousePos = getLocalPointerPos(ev, rect);
};
container.addEventListener('mousemove', handlePointerMove);
container.addEventListener('touchmove', handlePointerMove);
const initRender = (ev: MouseEvent | TouchEvent) => {
const rect = container.getBoundingClientRect();
this.mousePos = getLocalPointerPos(ev, rect);
this.cacheMousePos = { ...this.mousePos };
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);
}
private render() {
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);
if (distance > this.threshold) {
this.showNextImage();
this.lastMousePos = { ...this.mousePos };
}
if (this.isIdle && this.zIndexVal !== 1) this.zIndexVal = 1;
requestAnimationFrame(() => this.render());
}
private showNextImage() {
++this.zIndexVal;
this.imgPosition = this.imgPosition < this.imagesTotal - 1 ? this.imgPosition + 1 : 0;
const img = this.images[this.imgPosition];
++this.visibleImagesCount;
gsap.killTweensOf(img.DOM.el);
const scaleValue = gsap.utils.random(0.5, 1.6);
gsap
.timeline({
onStart: () => this.onImageActivated(),
onComplete: () => this.onImageDeactivated()
})
.fromTo(
img.DOM.el,
{
scale: scaleValue - Math.max(gsap.utils.random(0.2, 0.6), 0),
rotationZ: 0,
opacity: 1,
zIndex: this.zIndexVal,
x: this.cacheMousePos.x - (img.rect?.width ?? 0) / 2,
y: this.cacheMousePos.y - (img.rect?.height ?? 0) / 2
},
{
duration: 0.4,
ease: 'power3',
scale: scaleValue,
rotationZ: gsap.utils.random(-3, 3),
x: this.mousePos.x - (img.rect?.width ?? 0) / 2,
y: this.mousePos.y - (img.rect?.height ?? 0) / 2
},
0
);
if (this.visibleImagesCount >= this.visibleImagesTotal) {
const lastInQueue = getNewPosition(this.imgPosition, this.visibleImagesTotal, this.images);
const oldImg = this.images[lastInQueue];
gsap.to(oldImg.DOM.el, {
duration: 0.4,
ease: 'power4',
opacity: 0,
scale: 1.3,
onComplete: () => {
if (this.activeImagesCount === 0) {
this.isIdle = true;
}
}
});
}
}
private onImageActivated() {
this.activeImagesCount++;
this.isIdle = false;
}
private onImageDeactivated() {
this.activeImagesCount--;
}
}
class ImageTrailVariant8 {
private container: HTMLDivElement;
private DOM: { el: HTMLDivElement };
private images: ImageItem[];
private imagesTotal: number;
private imgPosition: number;
private zIndexVal: number;
private activeImagesCount: number;
private isIdle: boolean;
private threshold: number;
private mousePos: { x: number; y: number };
private lastMousePos: { x: number; y: number };
private cacheMousePos: { x: number; y: number };
private rotation: { x: number; y: number };
private cachedRotation: { x: number; y: number };
private zValue: number;
private cachedZValue: number;
constructor(container: HTMLDivElement) {
this.container = container;
this.DOM = { el: container };
this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img as HTMLDivElement));
this.imagesTotal = this.images.length;
this.imgPosition = 0;
this.zIndexVal = 1;
this.activeImagesCount = 0;
this.isIdle = true;
this.threshold = 80;
this.mousePos = { x: 0, y: 0 };
this.lastMousePos = { x: 0, y: 0 };
this.cacheMousePos = { x: 0, y: 0 };
this.rotation = { x: 0, y: 0 };
this.cachedRotation = { x: 0, y: 0 };
this.zValue = 0;
this.cachedZValue = 0;
const handlePointerMove = (ev: MouseEvent | TouchEvent) => {
const rect = container.getBoundingClientRect();
this.mousePos = getLocalPointerPos(ev, rect);
};
container.addEventListener('mousemove', handlePointerMove);
container.addEventListener('touchmove', handlePointerMove);
const initRender = (ev: MouseEvent | TouchEvent) => {
const rect = container.getBoundingClientRect();
this.mousePos = getLocalPointerPos(ev, rect);
this.cacheMousePos = { ...this.mousePos };
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);
}
private render() {
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);
if (distance > this.threshold) {
this.showNextImage();
this.lastMousePos = { ...this.mousePos };
}
if (this.isIdle && this.zIndexVal !== 1) {
this.zIndexVal = 1;
}
requestAnimationFrame(() => this.render());
}
private showNextImage() {
const rect = this.container.getBoundingClientRect();
const centerX = rect.width / 2;
const centerY = rect.height / 2;
const relX = this.mousePos.x - centerX;
const relY = this.mousePos.y - centerY;
this.rotation.x = -(relY / centerY) * 30;
this.rotation.y = (relX / centerX) * 30;
this.cachedRotation = { ...this.rotation };
const distanceFromCenter = Math.sqrt(relX * relX + relY * relY);
const maxDistance = Math.sqrt(centerX * centerX + centerY * centerY);
const proportion = distanceFromCenter / maxDistance;
this.zValue = proportion * 1200 - 600;
this.cachedZValue = this.zValue;
const normalizedZ = (this.zValue + 600) / 1200;
const brightness = 0.2 + normalizedZ * 2.3;
++this.zIndexVal;
this.imgPosition = this.imgPosition < this.imagesTotal - 1 ? this.imgPosition + 1 : 0;
const img = this.images[this.imgPosition];
gsap.killTweensOf(img.DOM.el);
gsap
.timeline({
onStart: () => this.onImageActivated(),
onComplete: () => this.onImageDeactivated()
})
.set(this.DOM.el, { perspective: 1000 }, 0)
.fromTo(
img.DOM.el,
{
opacity: 1,
z: 0,
scale: 1 + this.cachedZValue / 1000,
zIndex: this.zIndexVal,
x: this.cacheMousePos.x - (img.rect?.width ?? 0) / 2,
y: this.cacheMousePos.y - (img.rect?.height ?? 0) / 2,
rotationX: this.cachedRotation.x,
rotationY: this.cachedRotation.y,
filter: `brightness(${brightness})`
},
{
duration: 1,
ease: 'expo',
scale: 1 + this.zValue / 1000,
x: this.mousePos.x - (img.rect?.width ?? 0) / 2,
y: this.mousePos.y - (img.rect?.height ?? 0) / 2,
rotationX: this.rotation.x,
rotationY: this.rotation.y
},
0
)
.to(
img.DOM.el,
{
duration: 0.4,
ease: 'power2',
opacity: 0,
z: -800
},
0.3
);
}
private onImageActivated() {
this.activeImagesCount++;
this.isIdle = false;
}
private onImageDeactivated() {
this.activeImagesCount--;
if (this.activeImagesCount === 0) {
this.isIdle = true;
}
}
}
type ImageTrailConstructor =
| typeof ImageTrailVariant1
| typeof ImageTrailVariant2
| typeof ImageTrailVariant3
| typeof ImageTrailVariant4
| typeof ImageTrailVariant5
| typeof ImageTrailVariant6
| typeof ImageTrailVariant7
| typeof ImageTrailVariant8;
const variantMap: Record<number, ImageTrailConstructor> = {
1: ImageTrailVariant1,
2: ImageTrailVariant2,
3: ImageTrailVariant3,
4: ImageTrailVariant4,
5: ImageTrailVariant5,
6: ImageTrailVariant6,
7: ImageTrailVariant7,
8: ImageTrailVariant8
};
interface ImageTrailProps {
items?: string[];
variant?: number;
}
export default function ImageTrail({ items = [], variant = 1 }: ImageTrailProps): JSX.Element {
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!containerRef.current) return;
const Cls = variantMap[variant] || variantMap[1];
new Cls(containerRef.current);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [variant, items]);
return (
<div className="w-full h-full relative z-[100] rounded-lg bg-transparent overflow-visible" ref={containerRef}>
{items.map((url, i) => (
<div
className="content__img w-[190px] aspect-[1.1] rounded-[15px] absolute top-0 left-0 opacity-0 overflow-hidden [will-change:transform,filter]"
key={i}
>
<div
className="content__img-inner bg-center bg-cover w-[calc(100%+20px)] h-[calc(100%+20px)] absolute top-[-10px] left-[-10px]"
style={{ backgroundImage: `url(${url})` }}
/>
</div>
))}
</div>
);
}