Как мне сделать анимированный счетчик с помощью GSAP на реакте?
Как мне реализовать анимированный счетчик на gsap, или на anime.js, или на на React. Как показано по ссылке на пример. Состояние пагинации и массивы с датами лежат в slice redux-toolkit.
const initialState: PostsState = {
currentPage: 1,
dates: {
1: [2015, 2019],
2: [1987, 1990],
3: [2010, 2014],
4: [1926, 1930],
5: [2000, 2004],
6: [2020, 2024]
},
};
const postsSlice = createSlice({
name: 'posts',
initialState,
reducers: {
setCurrentPage(state, action: PayloadAction<number>) {
state.currentPage = action.payload;
},
}});
Ответы (2 шт):
Решение на GSAP
Этот кусок отвечает за анимацию, gsap плавно изменяет свойства объекта currentDate
, а потом выводится на экран
tl.to(currentDate, { dateStart: date.dateStart, dateEnd: date.dateEnd, onUpdate: () => {
renderCurrentDate(currentDate, currentDateNodes);
}});
Всё остальное - рендеринг html элементов на странице
const dates = [
{ dateStart: 2015, dateEnd: 2019 },
{ dateStart: 1987, dateEnd: 1990 },
{ dateStart: 2010, dateEnd: 2014 },
{ dateStart: 1926, dateEnd: 1930 },
{ dateStart: 2000, dateEnd: 2004 },
{ dateStart: 2020, dateEnd: 2024 },
];
const wrapperNode = renderWrapperNode();
const currentDate = {
dateStart: dates[0].dateStart,
dateEnd: dates[0].dateEnd
};
const periodBtnNodes = renderPeriodBtns(dates, wrapperNode);
const currentDateNodes = renderInitCurrentDate(currentDate, periodBtnNodes, wrapperNode);
const tl = gsap.timeline({ defaults: { duration: 2 } });
function renderWrapperNode() {
const wrapperNode = document.createElement('div');
wrapperNode.classList.add('wrapper');
document.body.append(wrapperNode);
return wrapperNode;
}
function renderPeriodBtns(dates, wrapperNode) {
const btnNodes = dates.map(date => {
const btnNode = document.createElement('button');
btnNode.classList.add('btn');
btnNode.type = 'button';
btnNode.textContent = `${date.dateStart} - ${date.dateEnd}`;
btnNode.dataset.dateStart = date.dateStart;
btnNode.dataset.dateEnd = date.dateEnd;
btnNode.addEventListener('click', (event) => clickPeriodBtnListener(event, date, currentDateNodes, tl, btnNode));
return btnNode;
});
btnNodes[0].classList.add('btn_active');
const periodBtnsWrapperNode = document.createElement('div');
periodBtnsWrapperNode.classList.add('period-btns-wrapper');
periodBtnsWrapperNode.append(...btnNodes);
wrapperNode.prepend(periodBtnsWrapperNode);
return btnNodes;
}
function renderInitCurrentDate(currentDate, periodBtnNodes, wrapperNode) {
const curentDateNode = document.createElement('div');
curentDateNode.classList.add('current-date');
curentDateNode.innerHTML = `
<div class="current-date__start">${currentDate.dateStart}</div>
<div class="current-date__end">${currentDate.dateEnd}</div>
`;
wrapperNode.append(curentDateNode);
const currentDateStartNode = curentDateNode.querySelector('.current-date__start');
const currentDateEndNode = curentDateNode.querySelector('.current-date__end');
return {
currentDateStartNode,
currentDateEndNode,
curentDateNode
};
}
function renderCurrentDate(newDate, {
currentDateStartNode,
currentDateEndNode,
}) {
currentDateStartNode.textContent = Math.round(newDate.dateStart);
currentDateEndNode.textContent = Math.round(newDate.dateEnd);
}
function clickPeriodBtnListener(event, date, currentDateNodes, tl, btnNode) {
if (tl.isActive() === true) return;
toggleActiveBtnClass(btnNode);
tl.to(currentDate, { dateStart: date.dateStart, dateEnd: date.dateEnd, onUpdate: () => {
renderCurrentDate(currentDate, currentDateNodes);
}});
}
function toggleActiveBtnClass(btnNode) {
const btnNodes = document.querySelectorAll('.period-btns-wrapper .btn');
for (const btnNodeItem of btnNodes) {
btnNodeItem.classList.remove('btn_active');
}
btnNode.classList.add('btn_active');
}
.wrapper {
display: flex;
flex-direction: column;
gap: 20px;
}
.period-btns-wrapper {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.btn {
border: 2px solid gray;
cursor: pointer;
}
.btn_active {
border-color: black;
cursor: default;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js" integrity="sha512-7eHRwcbYkK4d9g/6tD/mhkf++eoTHwpNM9woBxtPUBWm67zeAfFC+HrdoE2GanKeocly/VxeLvIqwvCdk7qScg==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
Если на react, то так, добавить useRef
для элементов с начальной и конечной датами, useState
для значений счетчиков и учета актуальных значений. В useEffect
инициализировать gsap
для анимации начальной и конечной точек с соответствующими зависимостями. Создать обработчик для изменения начальной и конечной дат и повесить на нужные элементы. Этот обработчик может использоваться как для блоков с датами, так для стрелочек перехода на следующую/предыдущую дату или отдельных блоков - достаточно передавать целевой индекс.
В рабочем примере скорректировано для запуска текущей платформе, на codeSandbox более каноничный вариант.
const dates = [
{ dateStart: 2015, dateEnd: 2019 },
{ dateStart: 1987, dateEnd: 1990 },
{ dateStart: 2010, dateEnd: 2014 },
{ dateStart: 1926, dateEnd: 1930 },
{ dateStart: 2000, dateEnd: 2004 },
{ dateStart: 2020, dateEnd: 2024 },
];
function App() {
const startEl = React.useRef();
const endEl = React.useRef();
const [current, setCurrent] = React.useState(0);
const [start, setStart] = React.useState(dates[current].dateStart || 0);
const [end, setEnd] = React.useState(dates[current].dateEnd || 0);
React.useEffect(() => {
window.gsap.to(startEl.current, {
innerText: start,
duration: 1,
snap: {
innerText: 1,
},
});
window.gsap.to(endEl.current, {
innerText: end,
duration: 1,
snap: {
innerText: 1,
},
});
}, [start, end]);
const handleClick = (idx) => {
if (idx < 0) return;
if (idx >= dates.length) return;
setCurrent(idx);
setStart(dates[idx].dateStart);
setEnd(dates[idx].dateEnd);
};
return (
<div className="App">
<h3>
<span ref={startEl}>{dates[0].dateStart}</span>
<b> - </b>
<span ref={endEl}>{dates[0].dateEnd}</span>
</h3>
<div className="nav">
<button onClick={() => handleClick(current - 1)}>назад</button>
<button onClick={() => handleClick(current + 1)}>вперед</button>
</div>
<br />
<div className="list">
{dates.map(({ dateStart, dateEnd }, idx) => (
<div
className="item"
onClick={() => handleClick(idx)}
key={(dateStart, dateEnd)}
>
<span>{dateStart}</span>
<b> - </b>
<span>{dateEnd}</span>
</div>
))}
</div>
</div>
);
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<App />
);
.App {
font-family: sans-serif;
text-align: center;
}
.list {
display: flex;
flex-wrap: wrap;
align-items: center;
}
.item {
padding: 10px;
font-size: 14px;
font-weight: 700;
margin: 7px;
border: 2px solid #bbb;
cursor: pointer;
}
.item:hover {
background-color: #bbb;
}
<script src="https://unpkg.com/react@18/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js" crossorigin></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js"></script>
<div id="root"></div>
В рабочем виде на codeSandbox: