From c57957f15cfca47884cd0e226569ae521e5c8c4c Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Sun, 23 Nov 2025 15:39:04 +0300 Subject: [PATCH] =?UTF-8?q?refactor:=20=D0=9F=D0=B5=D1=80=D0=B5=D1=80?= =?UTF-8?q?=D0=B0=D0=B1=D0=BE=D1=82=D0=BA=D0=B0=20=D0=BA=D0=BE=D0=BC=D0=BF?= =?UTF-8?q?=D0=BE=D0=BD=D0=B5=D0=BD=D1=82=D0=BE=D0=B2=20=D0=BD=D0=B0=20?= =?UTF-8?q?=D0=B8=D1=81=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7=D0=BE=D0=B2=D0=B0?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20=D1=85=D1=83=D0=BA=D0=B0=20useGSAP?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 1 + pnpm-lock.yaml | 14 ++++ .../ui/CircleTimeline/CircleTimeline.tsx | 84 ++++++++++--------- .../ui/EventsCarousel/EventsCarousel.tsx | 22 +++-- .../ui/TimeFrameSlider/TimeFrameSlider.tsx | 21 ++--- 5 files changed, 80 insertions(+), 62 deletions(-) diff --git a/package.json b/package.json index 2acf48a..a0592c5 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "author": "", "license": "ISC", "dependencies": { + "@gsap/react": "^2.1.2", "classnames": "^2.5.1", "gsap": "^3.13.0", "react": "^19.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 770ab62..49ec6d0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + '@gsap/react': + specifier: ^2.1.2 + version: 2.1.2(gsap@3.13.0)(react@19.2.0) classnames: specifier: ^2.5.1 version: 2.5.1 @@ -1111,6 +1114,12 @@ packages: resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@gsap/react@2.1.2': + resolution: {integrity: sha512-JqliybO1837UcgH2hVOM4VO+38APk3ECNrsuSM4MuXp+rbf+/2IG2K1YJiqfTcXQHH7XlA0m3ykniFYstfq0Iw==} + peerDependencies: + gsap: ^3.12.5 + react: '>=17' + '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} @@ -6799,6 +6808,11 @@ snapshots: '@eslint/core': 0.17.0 levn: 0.4.1 + '@gsap/react@2.1.2(gsap@3.13.0)(react@19.2.0)': + dependencies: + gsap: 3.13.0 + react: 19.2.0 + '@humanfs/core@0.19.1': {} '@humanfs/node@0.16.7': diff --git a/src/widgets/TimeFrameSlider/ui/CircleTimeline/CircleTimeline.tsx b/src/widgets/TimeFrameSlider/ui/CircleTimeline/CircleTimeline.tsx index f803983..a8f3dec 100644 --- a/src/widgets/TimeFrameSlider/ui/CircleTimeline/CircleTimeline.tsx +++ b/src/widgets/TimeFrameSlider/ui/CircleTimeline/CircleTimeline.tsx @@ -8,9 +8,10 @@ * Поддерживает клик по точкам для переключения периодов. */ +import { useGSAP } from '@gsap/react' import classNames from 'classnames' import { gsap } from 'gsap' -import { memo, useCallback, useEffect, useMemo, useRef } from 'react' +import { memo, useCallback, useMemo, useRef } from 'react' import styles from './CircleTimeline.module.scss' import { calculateCoordinates } from '../../lib/utils/calculateCoordinates/calculateCoordinates' @@ -71,52 +72,55 @@ export const CircleTimeline = memo(function CircleTimeline({ const titlesRef = useRef<(HTMLSpanElement | null)[]>([]) /** - * Эффект для анимации поворота круга и контр-поворота точек - * Запускается при изменении rotation + * Анимация поворота круга и контр-поворота точек + * Использует useGSAP hook для автоматической очистки анимаций */ - useEffect(() => { - // Анимация поворота контейнера круга - if (circleRef.current) { - gsap.to(circleRef.current, { - rotation, - duration: ANIMATION_DURATION, - ease: ANIMATION_EASE, - }) - } - - // Контр-поворот точек, чтобы текст оставался читаемым - pointsRef.current.forEach((point, index) => { - if (point) { - gsap.to(point, { - rotation: -rotation, - duration: 0, + useGSAP( + () => { + // Анимация поворота контейнера круга + if (circleRef.current) { + gsap.to(circleRef.current, { + rotation, + duration: ANIMATION_DURATION, ease: ANIMATION_EASE, }) + } - // Анимация заголовка - const title = titlesRef.current[index] - if (title) { - // Сбрасываем предыдущие анимации для этого элемента - gsap.killTweensOf(title) + // Контр-поворот точек, чтобы текст оставался читаемым + pointsRef.current.forEach((point, index) => { + if (point) { + gsap.to(point, { + rotation: -rotation, + duration: 0, + ease: ANIMATION_EASE, + }) - if (index === activeIndex) { - gsap.to(title, { - opacity: 1, - visibility: 'visible', - duration: 0.5, - delay: ANIMATION_DURATION, // Ждем окончания вращения - }) - } else { - gsap.to(title, { - opacity: 0, - visibility: 'hidden', - duration: 0.2, - }) + // Анимация заголовка + const title = titlesRef.current[index] + if (title) { + // Останавливаем предыдущие анимации для предотвращения конфликтов + gsap.killTweensOf(title) + + if (index === activeIndex) { + gsap.to(title, { + opacity: 1, + visibility: 'visible', + duration: 0.5, + delay: ANIMATION_DURATION, // Ждем окончания вращения + }) + } else { + gsap.to(title, { + opacity: 0, + visibility: 'hidden', + duration: 0.2, + }) + } } } - } - }) - }, [rotation, activeIndex]) + }) + }, + { dependencies: [rotation, activeIndex] } + ) /** * Мемоизированный расчет позиций точек на круге diff --git a/src/widgets/TimeFrameSlider/ui/EventsCarousel/EventsCarousel.tsx b/src/widgets/TimeFrameSlider/ui/EventsCarousel/EventsCarousel.tsx index 0e9c4df..54326ba 100644 --- a/src/widgets/TimeFrameSlider/ui/EventsCarousel/EventsCarousel.tsx +++ b/src/widgets/TimeFrameSlider/ui/EventsCarousel/EventsCarousel.tsx @@ -4,9 +4,10 @@ * Отображает список исторических событий в виде слайдера */ +import { useGSAP } from '@gsap/react' import classNames from 'classnames' import { gsap } from 'gsap' -import { memo, useEffect, useRef, useState } from 'react' +import { memo, useRef, useState } from 'react' import { Swiper, SwiperSlide } from 'swiper/react' import 'swiper/css' @@ -45,7 +46,7 @@ export interface EventsCarouselProps { * * Использует Swiper для создания слайдера с кастомной навигацией. * Поддерживает адаптивное количество слайдов на разных размерах экрана. - * Анимирует появление/исчезновение с помощью GSAP. + * Анимирует появление/исчезновение с помощью GSAP useGSAP hook. * * @example * ```tsx @@ -62,13 +63,11 @@ export const EventsCarousel = memo( const [isEnd, setIsEnd] = useState(false) /** - * Эффект для анимации появления/исчезновения карусели - * Использует GSAP для плавной анимации opacity и y-позиции + * Анимация появления/исчезновения карусели + * Использует useGSAP hook для автоматической очистки анимаций */ - useEffect(() => { - if (!containerRef.current) return - - const ctx = gsap.context(() => { + useGSAP( + () => { if (visible) { gsap.fromTo( containerRef.current, @@ -86,10 +85,9 @@ export const EventsCarousel = memo( duration: HIDE_DURATION, }) } - }, containerRef) - - return () => ctx.revert() - }, [visible, events]) + }, + { scope: containerRef, dependencies: [visible, events] } + ) /** * Обработчик инициализации Swiper diff --git a/src/widgets/TimeFrameSlider/ui/TimeFrameSlider/TimeFrameSlider.tsx b/src/widgets/TimeFrameSlider/ui/TimeFrameSlider/TimeFrameSlider.tsx index 5293b6d..2bfc39b 100644 --- a/src/widgets/TimeFrameSlider/ui/TimeFrameSlider/TimeFrameSlider.tsx +++ b/src/widgets/TimeFrameSlider/ui/TimeFrameSlider/TimeFrameSlider.tsx @@ -3,6 +3,7 @@ * Главный компонент временной шкалы с круговой диаграммой и каруселью событий */ +import { useGSAP } from '@gsap/react' import classNames from 'classnames' import { gsap } from 'gsap' import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react' @@ -21,7 +22,7 @@ import { EventsCarousel } from '../EventsCarousel/EventsCarousel' * * Отображает исторические периоды на круговой диаграмме с возможностью * переключения между ними. Для каждого периода показывается карусель событий. - * Центральные даты анимируются при смене периода с помощью GSAP. + * Центральные даты анимируются при смене периода с помощью GSAP useGSAP hook. * * @example * ```tsx @@ -66,13 +67,11 @@ export const TimeFrameSlider = memo(() => { }, [activePeriod, anglePerPoint]) /** - * Анимация центральных дат с использованием GSAP + * Анимация центральных дат с использованием GSAP useGSAP hook * Плавно изменяет числа при смене периода */ - useEffect(() => { - if (!containerRef.current) return - - const ctx = gsap.context(() => { + useGSAP( + () => { if (startYearRef.current) { gsap.fromTo( startYearRef.current, @@ -109,10 +108,12 @@ export const TimeFrameSlider = memo(() => { { opacity: 1, visibility: 'visible', duration: 1 } ) } - }, containerRef) - - return () => ctx.revert() - }, [currentPeriod.yearFrom, currentPeriod.yearTo]) + }, + { + scope: containerRef, + dependencies: [currentPeriod.yearFrom, currentPeriod.yearTo], + } + ) /** * Переключение на предыдущий период