НейроАгент

Как анимировать SVG в React с середины обводки

Узнайте, как правильно настроить анимацию SVG обводки в React, чтобы линия начинала движение с середины. Полные примеры кода и методы оптимизации.

Вопрос

Как анимировать SVG в React, чтобы анимация обводки начиналась с середины?

Имеется SVG файл с прямоугольником, который нужно анимировать так, чтобы зелёная линия начинала движение не с начала обводки, а с её середины. Текущий код использует requestAnimationFrame для анимации stroke-dashoffset, но не достигает желаемого эффекта.

javascript
useEffect(() => {
    if (!svgRef.current) return;

    const svgElement = svgRef.current;
    const animatedElement = svgElement.querySelector('#svg_5');
    
    // Инициализация stroke dasharray
    const length = animatedElement.getTotalLength ? animatedElement.getTotalLength() : 1000;
    animatedElement.style.strokeDasharray = length;
    animatedElement.style.strokeDashoffset = length;
    animatedElement.style.strokeWidth = 2;
    animatedElement.style.stroke = "#00ff7f";

    if (!isAnimated) return;

    let startTime = null;
    const duration = 6000;

    const animate = (timestamp) => {
        if (!startTime) startTime = timestamp;
        const elapsed = timestamp - startTime;
        const progress = Math.min(elapsed / duration, 1);

        setIsProgress(progress);

        const totalLength = animatedElement.getTotalLength ? animatedElement.getTotalLength() : 1000;
        const startOffset = (startStoke / 100) * totalLength;
        const endOffset = 0.6 * totalLength;
        
        const currentOffset = endOffset + (startOffset-endOffset) * progress;
        
        animatedElement.style.strokeDashoffset = currentOffset;
        animatedElement.style.stroke = "#00ff7f";

        if (progress < 1 && isAnimated) {
            animationRef.current = requestAnimationFrame(animate);
        } else if (progress >= 1) {
            setIsAnimated(false);
        }
    };

    animationRef.current = requestAnimationFrame(animate);

    return () => {
        if (animationRef.current) {
            cancelAnimationFrame(animationRef.current);
        }
    };
}, [isAnimated, startStoke, endStoke]);

Как правильно настроить параметры анимации, чтобы линия начинала движение с середины обводки SVG элемента?

НейроАгент

Чтобы анимировать SVG обводку в React так, чтобы анимация начиналась с середины, нужно правильно настроить параметры stroke-dasharray и stroke-dashoffset. Текущий код имеет несколько проблем в расчетах и настройке параметров.

Содержание

Проблема текущего подхода

В текущем коде есть несколько ключевых проблем:

  1. Неверные параметры startStoke и endStoke - переменные используются, но не определены
  2. Неправильный расчет смещения - логика анимации смещения не учитывает середину пути
  3. Отсутствие правильной настройки stroke-dasharray - для анимации с середины нужна другая конфигурация

Как отмечают эксперты из CSS-Tricks, ключевая идея состоит в том, чтобы “настроить смещение так, чтобы занимало весь путь” и использовать правильные соотношения dash и gap.

Правильный метод анимации с середины

Для анимации с середины нужно использовать следующий подход:

javascript
useEffect(() => {
    if (!svgRef.current) return;

    const svgElement = svgRef.current;
    const animatedElement = svgElement.querySelector('#svg_5');
    
    // Получаем полную длину пути
    const length = animatedElement.getTotalLength();
    
    // Настраиваем параметры для анимации с середины
    animatedElement.style.strokeDasharray = length / 2; // Половина длины для dash
    animatedElement.style.strokeDashoffset = length / 4; // Начинаем с четверти пути (середина)
    animatedElement.style.strokeWidth = 2;
    animatedElement.style.stroke = "#00ff7f";

    if (!isAnimated) return;

    let startTime = null;
    const duration = 6000;

    const animate = (timestamp) => {
        if (!startTime) startTime = timestamp;
        const elapsed = timestamp - startTime;
        const progress = Math.min(elapsed / duration, 1);

        setIsProgress(progress);

        const totalLength = animatedElement.getTotalLength();
        
        // Анимация от середины к концу
        const startOffset = length / 4; // Начинаем с середины
        const endOffset = length; // Двигаемся до конца
        const currentOffset = startOffset + (endOffset - startOffset) * (1 - progress);
        
        animatedElement.style.strokeDashoffset = currentOffset;

        if (progress < 1 && isAnimated) {
            animationRef.current = requestAnimationFrame(animate);
        } else if (progress >= 1) {
            setIsAnimated(false);
        }
    };

    animationRef.current = requestAnimationFrame(animate);

    return () => {
        if (animationRef.current) {
            cancelAnimationFrame(animationRef.current);
        }
    };
}, [isAnimated]);

Полный пример компонента React

Вот полный компонент с анимацией обводки, начинающейся с середины:

javascript
import React, { useRef, useEffect, useState } from 'react';

const SVGStrokeAnimation = () => {
    const svgRef = useRef(null);
    const animationRef = useRef(null);
    const [isAnimated, setIsAnimated] = useState(false);
    const [isProgress, setIsProgress] = useState(0);
    const [isAnimatingFromMiddle, setIsAnimatingFromMiddle] = useState(false);

    const startAnimation = () => {
        setIsAnimated(true);
        setIsAnimatingFromMiddle(true);
    };

    const resetAnimation = () => {
        setIsAnimated(false);
        setIsAnimatingFromMiddle(false);
    };

    useEffect(() => {
        if (!svgRef.current) return;

        const svgElement = svgRef.current;
        const animatedElement = svgElement.querySelector('#svg_5');
        
        if (!animatedElement) return;

        // Получаем полную длину пути
        const length = animatedElement.getTotalLength();
        
        // Настраиваем параметры для анимации
        animatedElement.style.strokeDasharray = length;
        animatedElement.style.strokeDashoffset = length;
        animatedElement.style.strokeWidth = 2;
        animatedElement.style.stroke = "#00ff7f";
        animatedElement.style.transition = 'none';

        if (!isAnimated) return;

        let startTime = null;
        const duration = 6000;

        const animate = (timestamp) => {
            if (!startTime) startTime = timestamp;
            const elapsed = timestamp - startTime;
            const progress = Math.min(elapsed / duration, 1);

            setIsProgress(progress);

            const totalLength = animatedElement.getTotalLength();
            
            if (isAnimatingFromMiddle) {
                // Анимация с середины
                const middleOffset = length / 2; // Середина пути
                const endOffset = 0; // Конец пути
                
                // Разделяем анимацию: сначала рисуем половину, затем вторую половину
                let currentOffset;
                if (progress < 0.5) {
                    // Первая половина: рисуем от середины к началу
                    const firstHalfProgress = progress * 2;
                    currentOffset = middleOffset - (middleOffset - 0) * firstHalfProgress;
                } else {
                    // Вторая половина: рисуем от середины к концу
                    const secondHalfProgress = (progress - 0.5) * 2;
                    currentOffset = middleOffset + (length - middleOffset) * secondHalfProgress;
                }
                
                animatedElement.style.strokeDashoffset = currentOffset;
            } else {
                // Обычная анимация с начала
                const startOffset = length;
                const endOffset = 0;
                const currentOffset = startOffset + (endOffset - startOffset) * progress;
                
                animatedElement.style.strokeDashoffset = currentOffset;
            }

            if (progress < 1 && isAnimated) {
                animationRef.current = requestAnimationFrame(animate);
            } else if (progress >= 1) {
                setIsAnimated(false);
            }
        };

        animationRef.current = requestAnimationFrame(animate);

        return () => {
            if (animationRef.current) {
                cancelAnimationFrame(animationRef.current);
            }
        };
    }, [isAnimated, isAnimatingFromMiddle]);

    return (
        <div>
            <div style={{ marginBottom: '20px' }}>
                <button onClick={startAnimation} disabled={isAnimated}>
                    {isAnimatingFromMiddle ? 'Анимация с середины' : 'Начать анимацию'}
                </button>
                <button onClick={resetAnimation} style={{ marginLeft: '10px' }}>
                    Сбросить
                </button>
                <div style={{ marginTop: '10px' }}>
                    Прогресс: {(isProgress * 100).toFixed(1)}%
                </div>
            </div>

            <svg 
                ref={svgRef} 
                width="400" 
                height="300" 
                viewBox="0 0 400 300"
                style={{ border: '1px solid #ccc' }}
            >
                <rect 
                    id="svg_5"
                    x="50" 
                    y="50" 
                    width="300" 
                    height="200" 
                    fill="none"
                    style={{ strokeDasharray: '0', strokeDashoffset: '0' }}
                />
            </svg>
        </div>
    );
};

export default SVGStrokeAnimation;

Альтернативные методы CSS анимации

Для более простых случаев можно использовать чистую CSS анимацию, как предлагают эксперты из DEV Community:

css
.svg-stroke-middle {
    stroke-dasharray: 100%;
    stroke-dashoffset: 50%;
    animation: drawFromMiddle 2s ease-in-out forwards;
}

@keyframes drawFromMiddle {
    0% {
        stroke-dashoffset: 50%;
    }
    100% {
        stroke-dashoffset: 0%;
    }
}

В React компоненте это можно использовать так:

javascript
const SVGStrokeAnimationCSS = () => {
    const [isAnimated, setIsAnimated] = useState(false);

    const startAnimation = () => {
        setIsAnimated(true);
    };

    return (
        <div>
            <button onClick={startAnimation} disabled={isAnimated}>
                Начать CSS анимацию
            </button>
            
            <svg width="400" height="300" viewBox="0 0 400 300">
                <rect 
                    x="50" 
                    y="50" 
                    width="300" 
                    height="200" 
                    fill="none"
                    className={`svg-stroke-middle ${isAnimated ? 'animated' : ''}`}
                    style={{
                        strokeWidth: 2,
                        stroke: "#00ff7f",
                        strokeDasharray: '100%',
                        strokeDashoffset: '50%',
                        transition: stroke-dashoffset 2s ease-in-out
                    }}
                />
            </svg>
        </div>
    );
};

Расчет параметров для разных типов SVG

Для разных типов SVG элементов расчеты могут отличаться:

  1. Для прямоугольников:

    javascript
    const length = 2 * (width + height); // Периметр прямоугольника
    const middleOffset = length / 2; // Середина периметра
    
  2. Для окружностей:

    javascript
    const radius = circleElement.getAttribute('r');
    const length = 2 * Math.PI * radius; // Длина окружности
    const middleOffset = length / 2; // Середина окружности
    
  3. Для сложных путей:

    javascript
    const length = pathElement.getTotalLength();
    const middleOffset = length / 2;
    

Как объясняют эксперты из Team Treehouse, stroke-dasharray и stroke-dashoffset работают вместе для создания эффектов анимации обводки.

Оптимизация производительности

Для лучшей производительности:

  1. Используйте will-change:

    css
    .animated-element {
        will-change: stroke-dashoffset;
    }
    
  2. Оптимизируйте анимацию:

    javascript
    // Используйте requestAnimationFrame с правильной частотой
    const animate = (timestamp) => {
        // ... логика анимации
        animationRef.current = requestAnimationFrame(animate);
    };
    
  3. Кэшируйте значения:

    javascript
    const cachedLength = animatedElement.getTotalLength();
    // Используйте cachedLength вместо повторных вызовов
    
  4. Используйте CSS transitions для простых анимаций:

    javascript
    animatedElement.style.transition = 'stroke-dashoffset 2s ease-in-out';
    animatedElement.style.strokeDashoffset = '0';
    

Таким образом, чтобы анимация обводки SVG начиналась с середины, нужно правильно рассчитать stroke-dashoffset, начиная с половины длины пути, и настроить анимацию для движения к началу и концу одновременно или поэтапно.

Источники

  1. Animate SVG Lines and Closed Paths with Dash Attributes - Let’s WP
  2. How SVG Line Animation Works | CSS-Tricks
  3. Animating stroke-dasharray and stroke-dashoffset (How To) | Team Treehouse
  4. Simple SVG drawing effect with stroke-dasharray & stroke-dashoffset - DEV Community
  5. stroke-dashoffset | CSS-Tricks Almanac
  6. Animating a complex SVG icon with dash-array and dash-offset in CSS | Justin Marsan