Код: Выделить всё
/* Enhanced scroll animation styles with forced continuous animation */
.scroll-wrapper {
overflow: hidden;
white-space: nowrap;
width: 100%;
transition: opacity 1s;
position: relative;
perspective: 1000px;
transform-style: preserve-3d;
backface-visibility: hidden;
-webkit-backface-visibility: hidden;
will-change: transform;
}
.scroll-content {
display: inline-block;
white-space: nowrap;
animation-name: continuous-scroll;
animation-timing-function: linear;
animation-iteration-count: infinite;
animation-play-state: running !important; /* Force animation to always play */
will-change: transform;
transform: translateZ(0);
backface-visibility: hidden;
-webkit-backface-visibility: hidden;
-webkit-font-smoothing: subpixel-antialiased;
-webkit-transform: translate3d(0, 0, 0);
-webkit-perspective: 1000;
}
/* Force animation during scrolling */
.force-animation {
animation-play-state: running !important;
}
@keyframes continuous-scroll {
0% {
transform: translate3d(0, 0, 0);
}
100% {
transform: translate3d(-50%, 0, 0);
}
}
< /code>
И это файл, в котором реализована анимация < /p>
'use client';
import { Skill as SkillType } from '@/types/skill';
import { skillTypeOrder } from '@/types/skill';
import { fetchSkills } from '@/services/skillService';
import { useTheme } from '@/context/ThemeContext';
import { Icon } from '@iconify/react';
import { useEffect, useState, useRef } from 'react';
import { motion } from 'framer-motion';
const Skill = () => {
const { isDarkMode } = useTheme();
const [isVisible, setIsVisible] = useState(false);
const [skillsData, setSkillsData] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
const [viewMode, setViewMode] = useState('scroll');
const scrollContentRefs = useRef([]);
const SCROLL_SPEED = 65;
const [isScrolling, setIsScrolling] = useState(false);
// Fetch skills data from the database
useEffect(() => {
const getSkillsData = async () => {
try {
const data = await fetchSkills();
setSkillsData(data);
setIsLoading(false);
} catch (err) {
console.error('Failed to fetch skills data:', err);
setError('Failed to load skills data');
setIsLoading(false);
}
};
getSkillsData();
setIsVisible(true);
}, []);
const groupedSkills = skillsData.reduce((acc, skill) => {
if (!acc[skill.type]) {
acc[skill.type] = [];
}
acc[skill.type].push(skill);
return acc;
}, {} as Record);
// Animation variants for the grid items
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.1
}
}
};
const itemVariants = {
hidden: { opacity: 0, y: 20 },
visible: {
opacity: 1,
y: 0,
transition: {
duration: 0.5
}
}
};
// Toggle view mode
const toggleViewMode = () => {
setViewMode(prev => prev === 'scroll' ? 'grid' : 'scroll');
};
// Enhanced scroll animation with prevention of stopping during scroll
useEffect(() => {
if (viewMode !== 'scroll' || !isVisible || skillsData.length === 0) return;
const adjustScrollAnimations = () => {
scrollContentRefs.current.forEach((ref, index) => {
if (!ref) return;
// Get the actual width of the first set of items
const children = Array.from(ref.children);
const halfPoint = children.length / 2;
// Calculate the width of the first set
let firstSetWidth = 0;
for (let i = 0; i < halfPoint; i++) {
const child = children[i] as HTMLElement;
if (child) {
// Include the element width and its margins
const style = window.getComputedStyle(child);
const marginLeft = parseInt(style.marginLeft);
const marginRight = parseInt(style.marginRight);
firstSetWidth += child.offsetWidth + marginLeft + marginRight;
}
}
// Calculate duration based on consistent scroll speed
// Use a minimum of 6 seconds to ensure smoother animations for small categories
const animDuration = Math.max(6, firstSetWidth / SCROLL_SPEED);
// Apply animation with forced GPU acceleration
ref.style.animationDuration = `${animDuration}s`;
ref.style.transform = 'translate3d(0, 0, 0)';
ref.style.willChange = 'transform';
// Ensure animation keeps running during scroll
ref.style.animationPlayState = 'running';
// Force a clean reflow
void ref.offsetWidth;
// Set animation direction based on index
const direction = index % 2 === 0 ? 'normal' : 'reverse';
ref.style.animationDirection = direction;
});
};
// Run initial animation setup
setTimeout(adjustScrollAnimations, 50);
setTimeout(adjustScrollAnimations, 300);
setTimeout(adjustScrollAnimations, 1000);
// Handle scroll events specifically to prevent animation pausing
let scrollTimeout: NodeJS.Timeout;
const handleScroll = () => {
setIsScrolling(true);
// Force animations to keep running during scroll
scrollContentRefs.current.forEach(ref => {
if (ref) {
ref.style.animationPlayState = 'running';
}
});
clearTimeout(scrollTimeout);
scrollTimeout = setTimeout(() => {
setIsScrolling(false);
}, 100);
};
// Handle resize events
const handleResize = () => {
// Clear animations temporarily to avoid jank
scrollContentRefs.current.forEach(ref => {
if (ref) {
ref.style.animationPlayState = 'paused';
}
});
// Small delay to allow resize to complete
setTimeout(() => {
adjustScrollAnimations();
// Resume animations
scrollContentRefs.current.forEach(ref => {
if (ref) {
ref.style.animationPlayState = 'running';
}
});
}, 100);
};
// Add scroll event listener
window.addEventListener('scroll', handleScroll, { passive: true });
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('scroll', handleScroll);
window.removeEventListener('resize', handleResize);
clearTimeout(scrollTimeout);
};
}, [viewMode, isVisible, skillsData, groupedSkills]);
// Create a callback ref setter function that fixes the TypeScript error
const setScrollContentRef = (element: HTMLDivElement | null, index: number) => {
scrollContentRefs.current[index] = element;
};
return (
Skills
{/* View mode toggle */}
setViewMode('scroll')}
className={`px-4 py-2 rounded-full text-sm font-medium transition-all duration-300 ${viewMode === 'scroll' ? 'bg-[var(--button-bg)] text-white shadow-sm' : 'text-[var(--card-text)] hover:bg-[var(--hover-bg)]'}`}
>
Scroll
setViewMode('grid')}
className={`px-4 py-2 rounded-full text-sm font-medium transition-all duration-300 ${viewMode === 'grid' ? 'bg-[var(--button-bg)] text-white shadow-sm' : 'text-[var(--card-text)] hover:bg-[var(--hover-bg)]'}`}
>
Grid
{isLoading ? (
) : error ? (
{error}
) : skillsData.length === 0 ? (
No skills available
) : (
// Define the priority order for skill types
(() => {
// Sort the skill types according to the defined order
return Object.entries(groupedSkills)
.sort(([typeA], [typeB]) => {
const indexA = skillTypeOrder.indexOf(typeA);
const indexB = skillTypeOrder.indexOf(typeB);
// If both types are in the priority list, sort by their position
if (indexA >= 0 && indexB >= 0) {
return indexA - indexB;
}
// If only typeA is in the list, it comes first
if (indexA >= 0) return -1;
// If only typeB is in the list, it comes first
if (indexB >= 0) return 1;
// If neither is in the list, maintain alphabetical order
return typeA.localeCompare(typeB);
});
})()
.map(([type, skills]: [string, SkillType[]], typeIndex: number) => (
{type}
{viewMode === 'scroll' ? (
// Scrolling view with fixed ref callback
setScrollContentRef(el, typeIndex)}
className={`scroll-content ${isScrolling ? 'force-animation' : ''}`}
style={{
// Default animation duration that will be overridden by useEffect
// Set a reasonable default to avoid initial stutter
animationDuration: '20s',
animationDirection: typeIndex % 2 === 0 ? 'normal' : 'reverse'
}}
>
{/* First set of items */}
{skills.map((skill: SkillType) => (
{skill.name}
))}
{/* Duplicate set for seamless scrolling */}
{skills.map((skill: SkillType) => (
key={`second-${skill.id}`}
className="inline-flex items-center space-x-2 md:space-x-3 px-4 md:px-6 py-3 md:py-4 mx-6 md:mx-12 rounded-xl border border-[var(--border-color)] bg-[var(--card-bg)] hover:bg-[var(--hover-bg)] transition-all duration-300 transform hover:scale-90"
>
{skill.name}
))}
) : (
// Grid view with animation
{skills.map((skill: SkillType) => (
{skill.name}
))}
)}
))
)}
);
};
export default Skill;
< /code>
Пожалуйста, дайте мне несколько предложений, как сделать анимацию более плавной без отставания. а также что может это вызвать? Lang-HTML PrettyPrint-Override ">
Scroll Animation Test
body {
margin: 0;
font-family: sans-serif;
background: #f5f5f5;
}
.scroll-wrapper {
overflow: hidden;
white-space: nowrap;
width: 100%;
height: 100px;
background: #222;
padding: 20px 0;
}
.scroll-content {
display: inline-block;
white-space: nowrap;
animation-name: continuous-scroll;
animation-timing-function: linear;
animation-iteration-count: infinite;
animation-duration: 20s;
animation-play-state: running;
will-change: transform;
transform: translate3d(0, 0, 0);
}
.item {
display: inline-block;
margin: 0 30px;
padding: 10px 20px;
background: #fff;
color: #222;
border-radius: 8px;
font-weight: bold;
font-size: 18px;
}
@keyframes continuous-scroll {
0% {
transform: translate3d(0, 0, 0);
}
100% {
transform: translate3d(-50%, 0, 0);
}
}
HTML
CSS
JS
React
Node
TypeScript
Tailwind
HTML
CSS
JS
React
Node
TypeScript
Tailwind
Подробнее здесь: https://stackoverflow.com/questions/796 ... is-lagging