const arrCards = [
{
key: 1,
}, {
key: 2,
}, {
...
}
]
<div class="card-wrap">
<div class="card">1</div>
<div class="card">2</div>
<div class="card">...</div>
</div>
getDefaultStyles() { // нужно создать метод, который будет определять
исходное состояние для каждого элемента (массив объектов)
return this.props.children.map(()=> {
return {
opacity: 0,
translateY: 50,
};
});
}
getStyles(prevInterpolatedStyles) { // также метод, который будет описывать саму анимацию, использую
предыдущие состояния (они передаются аргументом)
return prevInterpolatedStyles.map((_, i) => {
if (i === 0) {
return {
opacity: spring(1, [30, 11]), // функция spring, грубо говоря, задает тип анимации, на
подобии ease, ease-in-out и т.д.
translateY: spring(0, [30, 11]),
};
} else {
return {
opacity: spring(prevInterpolatedStyles[i - 1].opacity),
translateY: spring(prevInterpolatedStyles[i - 1].translateY),
};
}
});
}
render() {
return (
// Компонент <StaggeredMotion> как раз рассчитан на анимирование коллекции элементов
// В children'ы передается функция, которая будет вызваться всю анимацию
<StaggeredMotion defaultStyles={this.getDefaultStyles()} styles={this.getStyles}>
{(interpolatingStyles) =>
<div className="card-wrap">
{
interpolatingStyles.map((item, i) => {
return (
<div
key={this.props.children[i].key}
className="card"
style={{
// собственно значения, которое высчитываются в getStyles
(изначально состояние выставляется из getDefaultStyles)
opacity: `${item.opacity}`,
transform: `translateY(${item.translateY}px)`
}}
datatype={this.props.children[i].type}>
{this.props.children[i].key}
</div>
);
})
}
</div>
}
</StaggeredMotion>
);
}
Тоже самое, что и react-motion, но своими руками и оперируя классами
(setState + setTimeout + Recursion =
)
.card-appear,
.card-enter {
opacity: 0;
transform: translateY(50px);
}
.card-appear-active,
.card-enter-active {
opacity: 1;
transform: translateY(0);
transition: opacity 0.4s ease,
transform 0.4s ease;
}
.card-leave {
opacity: 1;
}
.card-leave-active {
opacity: 0;
transition: opacity 0.5s ease;
}
В данном случае встает вопрос как получить DOM элемент, который нужно анимировать. У нас есть следующие варианты
getElementById
, querySelector
и т.д.)findDOMNode
ref
у компонентов (ref={ (node) => { ... } }
)Для нашей задачи нам достаточно будет ref
.
render() {
const arrCards = this.props.children;
return (
<div className="card-wrap">
{
arrCards.map((item) => {
return (
<div
key={item.key}
className="card"
// заносим в локальную коллекцию все эл-ты
ref={(elem) => { this.arrCards.push(elem); }}
>
{item.key}
</div>
);
})
}
</div>
);
}
fadeIn() {
let appearTime = 400;
let nextElementDelay = 0.2
let animeJS = anime.timeline();
animeJS.add({
targets: this.arrCards,
duration: 1,
opacity: 0,
translateY: 50,
});
let index = 0;
arrCards.forEach((elem) => {
let delay = index++ * nextElementDelay * 1000;
animeJS.add({
targets: elem,
duration: appearTime,
opacity: 1,
translateY: 0,
easing: 'linear',
offset: delay,
});
});
}
fadeIn() {
let appearTime = 0.4;
let nextElementDelay = 0.2;
let timeline = new TimelineLite();
timeline
.staggerFromTo(this.arrCards, appearTime, {
display: 'none',
opacity: 0,
y: 50,
}, {
display: 'block',
opacity: 1,
y: 0,
}, nextElementDelay);
}
Но, так или иначе, и React way, и Another way позволяют решать одни и те же задачи, если оба способа заставить работать в содружестве.
// Web Animations
var animation = new Animation(element, {"transform": "translate(100px, 200px)"}, 1500);
// GSAP
var animation = TweenMax(element, 1.5, { x: 100, y: 200 });
.set
.to
.fromTo
.delay
.clear
.call
.addLabel
.eventCallback
Плашка должна появляться сверху вниз и исчезать в обратном порядке, то есть снизу вверх.
import TransitionGroup from 'TransitionGroup';
import TimelimeLite from 'gsap/TimelimeLite';
import 'gsap/CSSPlugin';
import 'gsap/EasePack';
function Notice(props) {
const { message, ...props } = props;
return (
<div className="notice" {...props}>
{message}
</div>
);
}
export default function (props) {
const { animate, duration, message } = props;
if (animate) {
return (
<TransitionGroup>
{ message ? <NoticeContainer message={message} duration={duration} /> : null }
</TransitionGroup>
);
} else {
return message ? Notice({ message }) : null;
}
};
class NoticeContainer extends Component {
defaultProps = {
duration: 0.7, // длительность анимации
};
elem = null; // DOM эл-т
rect = {}; // объект с координатами эл-та
height = 0; // высота эл-та
timeline = new TimelineLite(); // GSAP
componentDidMount() {
this.show();
}
// callback дает знать, что компонент готов к удалению
componentWillLeave(callback) {
this.hide(callback);
}
componentWillUnmount() {
// очищаем объект таймлайна, чтобы он не хранил ссылку на DOM эл-т
this.timeline = null;
}
show() {
this.rect = this.elem.getBoundingClientRect();
this.height = this.rect.height + this.rect.top;
this.timeline
.fromTo(this.elem, this.props.duration, {
y: `-${this.height}px`,
}, {
y: 0,
ease: Bounce.easeOut,
});
}
hide(onComplete) {
this.timeline
.to(this.elem, this.props.duration, {
y: `-${this.height}px`,
onComplete: onComplete,
});
}
getNode(node) {
this.elem = node;
}
render() {
return Notice({
ref: this.getNode.bind(this),
message: this.props.message,
});
}
}