Я заметил изменения в макете моего приложения React, особенно при анимации компонентов по порядку. Проблема возникает, когда я пытаюсь анимировать заголовок с помощью анимации набора текста, а затем анимировать основной текст с эффектом постепенного появления. Хотя анимации работают нормально по отдельности, при совместном использовании я замечаю, что контент «дергается» и меняет размер во время анимации, хотя я пытался исправить макет с помощью минимальной высоты.
Вот сценарий:< /p>
Heading uses a typing animation where the text appears one character at a time.
Body text uses a fade-in animation.
Когда оба этих компонента анимируются последовательно, происходит заметный сдвиг макета, и кажется, что содержимое меняет размер и перемещается по мере выполнения анимации.
Что я получил пробовал:
Adding min-height to prevent content from resizing, but this hasn’t fully solved the issue.
The problem does not occur when the animations are run without order (i.e., both components are animated simultaneously).
Чего я хочу достичь:
I want to animate the content in order (first the heading, then the body text) without causing layout shifts or resizing.
I would like to understand if there’s a better way to manage the animation order or any other workaround to avoid these shifts.
import React, { useState } from "react";
import { motion } from "framer-motion";
import Heading from "../Heading/Heading";
import BodyText from "../BodyText/BodyText";
import AnimatedBackground from "../../utilities/AnimatedBackground/AnimatedBackground";
import Marquee from "../Marquee/Marquee";
import { theme } from "../../theme";
const slideContent = {
title: "Innovative Solutions Through",
subtitle: "Custom Software Development",
description:
"Empowering businesses with tailored, cutting-edge tech solutions that transform ideas into impactful realities.",
};
const Hero = () => {
const [animationStep, setAnimationStep] = useState(0);
const handleTitleComplete = () => setAnimationStep(1);
const handleSubtitleComplete = () => setAnimationStep(2);
const handleDescriptionComplete = () => setAnimationStep(3);
return (
className={` ${theme.layoutPages.paddingHorizontal} w-full grid grid-cols-12 items-center text-center py-6 relative z-10 flex-grow`}
>
{/* Title Animation */}
{/* Subtitle Animation */}
{animationStep >= 1 && (
)}
{/* Description Animation */}
{animationStep >= 2 && (
)}
{/* Marquee Component */}
);
};
export default Hero;
import React from 'react';
import PropTypes from 'prop-types';
import { motion } from 'framer-motion';
import useTypingAnimation from '../../utilities/Animations/useTypingAnimation.js';
const Heading = ({
text,
spanText = '',
spanColor = 'text-neon',
color = 'text-white',
size = 'text-50px',
centered = true,
fontFamily = 'font-monument',
fontWeight = 'font-normal',
isAnimate = true,
order = 0,
speedMultiplier = 0.7,
onAnimationComplete,
className = '',
breakSpan = false,
}) => {
const parts = spanText ? text.split(spanText) : [text];
const { controls, ref, characterVariants } = useTypingAnimation({
text,
isAnimate,
order,
speedMultiplier,
});
// Split text into words while preserving spaces
const splitIntoWords = (string) => {
return string.split(/(\s+)/).filter(word => word.length > 0);
};
// Split words into characters for animation
const splitWordIntoChars = (word) => {
return word.split('').map((char) => (char === ' ' ? '\u00A0' : char));
};
const totalLength = text.length;
let charCount = 0;
// Render without animations if isAnimate is false
if (!isAnimate) {
return (
{parts[0]}
{spanText && (
{!parts[0].endsWith(' ') && ' '}
{spanText}
{!parts[1]?.startsWith(' ') && ' '}
)}
{parts[1]}
);
}
// Render with animations if isAnimate is true
return (
{/* First part */}
{splitIntoWords(parts[0]).map((word, wordIndex, wordArray) => (
{splitWordIntoChars(word).map((char, charIndex) => {
const currentCharIndex = charCount++;
return (
{char}
);
})}
))}
{/* Span text */}
{spanText && (
{!parts[0].endsWith(' ') && (
{'\u00A0'}
)}
{splitIntoWords(spanText).map((word, wordIndex) => (
{splitWordIntoChars(word).map((char, charIndex) => {
const currentCharIndex = charCount++;
return (
{char}
);
})}
))}
{!parts[1]?.startsWith(' ') && (
{'\u00A0'}
)}
)}
{/* Second part */}
{parts[1] && splitIntoWords(parts[1]).map((word, wordIndex) => (
{splitWordIntoChars(word).map((char, charIndex) => {
const currentCharIndex = charCount++;
return (
{char}
);
})}
))}
);
};
Heading.propTypes = {
text: PropTypes.string.isRequired,
spanText: PropTypes.string,
spanColor: PropTypes.string,
color: PropTypes.string,
size: PropTypes.string,
centered: PropTypes.bool,
fontFamily: PropTypes.string,
fontWeight: PropTypes.string,
isAnimate: PropTypes.bool,
order: PropTypes.number,
onAnimationComplete: PropTypes.func,
className: PropTypes.string,
breakSpan: PropTypes.bool,
};
export default Heading;
import React from 'react';
import PropTypes from 'prop-types';
import { motion } from 'framer-motion';
import useFadeInAnimation from '../../utilities/Animations/useFadeInAnimation';
const BodyText = ({
text,
color = 'text-white',
size = 'text-35px',
lineHeight = 'leading-normal',
fontFamily = 'font-mulish',
fontWeight = 'font-extralight',
centered = true,
isAnimate = true,
delay = 0,
onAnimationComplete = null,
className = '',
}) => {
// Initialize animation hooks
const { controls, ref, fadeInVariants } = useFadeInAnimation({ isAnimate, delay });
// Render without animation if `isAnimate` is false
if (!isAnimate) {
return (
className={`${centered ? 'text-center' : ''} ${color} ${size} ${lineHeight} ${fontFamily} ${fontWeight} ${className}`}
>
{text}
);
}
// Render with animation
return (
{text}
);
};
BodyText.propTypes = {
text: PropTypes.string.isRequired,
color: PropTypes.string,
size: PropTypes.string,
lineHeight: PropTypes.string,
fontFamily: PropTypes.string,
fontWeight: PropTypes.string,
centered: PropTypes.bool,
isAnimate: PropTypes.bool,
delay: PropTypes.number,
onAnimationComplete: PropTypes.func,
className: PropTypes.string,
};
export default BodyText;
import { useAnimation } from 'framer-motion';
import { useInView } from 'react-intersection-observer';
import { useEffect } from 'react';
const useTypingAnimation = ({ text, isAnimate, speedMultiplier = 0.8 }) => {
const controls = useAnimation();
const [ref, inView] = useInView({
triggerOnce: true,
threshold: 0.5,
});
const characterDelay = 0.06 * speedMultiplier;
useEffect(() => {
if (inView && isAnimate) {
controls.start('visible');
}
}, [inView, isAnimate, controls]);
const characterVariants = {
hidden: {
opacity: 0,
},
visible: i => ({
opacity: 1,
transition: {
delay: i * characterDelay,
duration: 0.05 * speedMultiplier,
},
}),
};
return { controls, ref, characterVariants };
};
export default useTypingAnimation;
import { useAnimation } from 'framer-motion';
import { useInView } from 'react-intersection-observer';
import { useEffect } from 'react';
const useFadeInAnimation = ({ isAnimate = true, delay = 0.3, duration = 1, threshold = 0.5 }) => {
const controls = useAnimation();
const [ref, inView] = useInView({
triggerOnce: true, // Only animate once when in view
threshold, // Controls when the animation is triggered (default 50% visibility)
});
useEffect(() => {
if (inView && isAnimate) {
controls.start('visible');
}
}, [inView, isAnimate, controls]);
const fadeInVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: { delay, duration, ease: 'easeInOut',
},
},
};
return { controls, ref, fadeInVariants };
};
export default useFadeInAnimation;
Подробнее здесь: https://stackoverflow.com/questions/792 ... in-effects
Сдвиги макета в React во время упорядоченной анимации (эффекты ввода и затухания) ⇐ CSS
-
- Похожие темы
- Ответы
- Просмотры
- Последнее сообщение
-
-
Почему генератор случайных чисел xorshift всегда использует именно эти сдвиги?
Anonymous » » в форуме C++ - 0 Ответы
- 15 Просмотры
-
Последнее сообщение Anonymous
-
-
-
Почему генератор случайных чисел xorshift всегда использует именно эти сдвиги?
Anonymous » » в форуме C++ - 0 Ответы
- 19 Просмотры
-
Последнее сообщение Anonymous
-