mirror of
https://github.com/LifeArchiveProject/WeChatDataAnalysis.git
synced 2026-02-19 22:30:49 +08:00
- 后端新增 card_04_emoji_universe:统计表情包/emoji 使用与画像 - 前端新增 Card04EmojiUniverse + VueBits Stack/ImageTrail 交互展示 - 更新 Wrapped manifest/Hero 预览与用例覆盖
1226 lines
37 KiB
Vue
1226 lines
37 KiB
Vue
<script setup lang="ts">
|
|
import { nextTick, onMounted, useTemplateRef } from 'vue';
|
|
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;
|
|
const 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;
|
|
const 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;
|
|
}
|
|
|
|
const props = withDefaults(defineProps<ImageTrailProps>(), {
|
|
items: () => [],
|
|
variant: 1
|
|
});
|
|
|
|
const containerRef = useTemplateRef<HTMLDivElement>('containerRef');
|
|
|
|
onMounted(async () => {
|
|
await nextTick();
|
|
|
|
if (!containerRef.value) return;
|
|
|
|
const Cls = variantMap[props.variant] || variantMap[1];
|
|
new Cls(containerRef.value);
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<div ref="containerRef" class="z-[100] relative bg-transparent rounded-lg w-full h-full overflow-visible">
|
|
<div
|
|
v-for="(url, i) in items"
|
|
:key="i"
|
|
class="top-0 left-0 absolute opacity-0 rounded-[15px] w-[190px] aspect-[1.1] overflow-hidden [will-change:transform,filter] content__img"
|
|
>
|
|
<div
|
|
class="top-[-10px] left-[-10px] absolute bg-cover bg-center w-[calc(100%+20px)] h-[calc(100%+20px)] content__img-inner"
|
|
:style="{ backgroundImage: `url(${url})` }"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
|
|
|