Tại sao sử dụng kỹ thuật FLIP
Đã bao lần bạn cần làm animate cho các property height
, width
, top
, left
? Bạn có để ý là những animate như vậy thường sẽ hơi khực khực. Lý do? những property này trigger layout change, trình duyệt sẽ xem xét các element khác có cần thay đổi gì không, việc này sẽ tiêu tốn sức người, tiền bạc của trình duyệt. Xem thêm bài viết Pixel are Expensive tác giả Paul Lewis sẽ nói rõ hơn.
Nói một cách khác, chúng ta muốn việc tính toán này hạn chế ở mức tối đa, nhanh nhất có thể. Mục tiêu là chúng ta chỉ animate trên transform
và opacity
. FLIP giải thích làm sao để chúng ta có thể đạt được layout change với chỉ property transform
FLIP là gì
FLIP là viết tắt của First, Last, Invert, Play
- First trước khi mọi thứ bắt đầu, lưu lại giá trị position và kích thước của element muốn transition. Có thể sử dụng
element.getBoundingClientRect()
- Last thực thi đoạn code sẽ gây ra transition trong khoản thời gian gần như là tức thì, lưu lại giá trị position và kích thước của element lúc đó.
- Invert do element đang ở vị trí cuối cùng, chúng ta muốn user nghĩ đó là ví trí đầu tiên, bằng cách sử dụng
transform
để thay đổi lại position và kích thước. Tính toán xíu, nhưng không thành vấn đề. - Play với element đã bị invert, chúng ta lại move nó lại vào vị trí cuối một lần nữa bằng
transform: none
Implement bên dưới sử dụng Web Animation API
const elm = document.querySelector('.some-element');
const first = elm.getBoundingClientRect();
// chạy đoạn script thực hiện change layout
doSomething();
// last: lấy giá trị cuối
const last = elm.getBoundingClientRect();
// invert: xác định sự khác nhau giữa giá trị first và last để mà invert
const deltaX = first.left - last.left;
const deltaY = first.top - last.top;
const deltaW = first.width / last.width;
const deltaH = first.height / last.height;
// Play
elm.animate([{
transformOrigin: 'top left',
transform: `
translate(${deltaX}px, ${deltaY}px)
scale(${deltaW}, ${deltaH})
`
}, {
transformOrigin: 'top left',
transform: 'none'
}], {
duration: 300,
easing: 'ease-in-out',
fill: 'both'
});
Lưu ý Web Animation API chưa support bởi tất cả trình duyệt, dùng polyfill
See the Pen How the FLIP technique works by David Khourshid (@davidkpiano) on CodePen.
Có 2 điểm quan trọng cần lưu ý
- Khi element thay đổi kích thước, khi dùng
scale
sẽ không ảnh hưởng performance, tuy nhiên nhớ đặttransformOrigin: 'top left'
- Đang sử dụng Web Animation API, nhưng ý tưởng này có thể hiện thực bằng GSAP, Anime, Velocity, Just-Animate, Mo.j hoặc bất kỳ thư viện animation khác
Shared element transition
Một trường hợp trong transition là element giữa các view hoặc giữa các trạng thái của trang, không phải lúc nào element ở vị trí cuối cũng giống như element lúc bắt đầu.
const firstElm = document.querySelector('.first-element');
// First
const first = firstElm.getBoundingClientRect();
firstElm.style.setProperty('visibility', 'hidden');
// chạy đoạn script thực hiện change layout
doSomething();
// Last
const lastElm = document.querySelector('.last-element');
const last = lastElm.getBoundingClientRect();
// giống như các bước ở trên
// ở đây chúng ta đang animate lastElm, không phải firstElm
Parent-child transition
Với ví dụ trên, chúng ta đo element với window
, trong đa số các trường hợp thì ok, tuy nhiên xét thử tình huống
- Element thay đổi vị trí và cần transition
- Element chứa đóng element con, các element con này cũng cần transition vị trí mới theo vị trí của parent
Để giải quyết, chúng ta cần đảm bảo việc tính toán theo giá trị relative với parent
const parentElm = document.querySelector('.parent');
const childElm = document.querySelector('.parent > .child');
// First: parent, child
const parentFirst = parentElm.getBoundingClientRect();
const childFirst = childElm.getBoundingClientRect();
doSomething();
// Last: parent and child
const parentLast = parentElm.getBoundingClientRect();
const childLast = childElm.getBoundingClientRect();
// Invert: parent
const parentDeltaX = parentFirst.left - parentLast.left;
const parentDeltaY = parentFirst.top - parentLast.top;
// Invert: child relative to parent
const childDeltaX = (childFirst.left - parentFirst.left)
- (childLast.left - parentLast.left);
const childDeltaY = (childFirst.top - parentFirst.top)
- (childLast.top - parentLast.top);
// Play: dùng WAAPI
parentElm.animate([
{ transform: `translate(${parentDeltaX}px, ${parentDeltaY}px)` },
{ transform: 'none' }
], { duration: 300, easing: 'ease-in-out' });
childElm.animate([
{ transform: `translate(${childDeltaX}px, ${childDeltaY}px)` },
{ transform: 'none' }
], { duration: 300, easing: 'ease-in-out' });
Một vài điểm cần lưu ý
- Giá trị thời gian cho parent và child (duration, easing) không nhất thiết phải khớp, tự do sáng tạo đi!
- Thay đổi kích thước ở parent hoặc child (width, height) không sử dụng ở đây để tránh phức tạp quá ví dụ này
- Có thể kết hợp giữa shared element và parent child cho kết quả dữ dội hơn
Sử dụng Flipping.js
Những kỹ thuật trình bày ở trên rất dễ hiểu, tuy nhiên sẽ hơi rối nếu chúng ta phải từ mò và theo dõi từng element. Tác giả bài viết này đã tạo ra một thư viện là Flipping.js để chúng ta xài cho sướng. Thêm vào data-flip-key="..."
vào element làm animate, chúng ta dễ dàng theo dõi được những element có thể thay đổi
<section class="gallery">
<div class="photo-1" data-flip-key="photo-1">
<img src="/photo-1"/>
</div>
<div class="photo-2" data-flip-key="photo-2">
<img src="/photo-2"/>
</div>
<div class="photo-3" data-flip-key="photo-3">
<img src="/photo-3"/>
</div>
</section>
<section class="details">
<div class="photo" data-flip-key="photo-1">
<img src="/photo-1"/>
</div>
<p class="description">
Lorem ipsum dolor sit amet...
</p>
</section>
Initializing...