Chào mừng bạn đến với Cafedev, nơi bạn có thể khám phá thế giới JavaScript từ cơ bản đến nâng cao. Trong bài viết này, chúng ta sẽ tìm hiểu về JavaScript animations, một công cụ mạnh mẽ giúp bạn tạo ra các hiệu ứng động mà CSS không thể thực hiện. Bằng cách sử dụng các kỹ thuật như `setInterval` và `requestAnimationFrame`, bạn sẽ học cách làm cho các yếu tố trên trang web của bạn hoạt động mượt mà và ấn tượng. Hãy cùng Cafedev khám phá và áp dụng những kỹ thuật này vào dự án của bạn!
JavaScript animations có thể xử lý những điều mà CSS không thể.
Ví dụ, di chuyển dọc theo một đường phức tạp, với hàm thời gian khác với đường cong Bezier, hoặc một animation trên canvas.
Nội dung chính
1. Sử dụng setInterval
Một animation có thể được triển khai như một chuỗi các khung hình — thường là các thay đổi nhỏ đối với các thuộc tính HTML/CSS.
Ví dụ, thay đổi style.left
từ 0px
đến 100px
sẽ di chuyển phần tử. Và nếu chúng ta tăng nó trong setInterval
, thay đổi 2px
với một độ trễ nhỏ, như 50 lần mỗi giây, thì nó sẽ trông mượt mà. Đó là nguyên tắc tương tự như trong rạp chiếu phim: 24 khung hình mỗi giây là đủ để làm cho nó trông mượt mà.
Mã giả có thể trông như thế này:
let timer = setInterval(function() {
if (animation complete) clearInterval(timer);
else increase style.left by 2px
}, 20); // change by 2px every 20ms, about 50 frames per second
Ví dụ hoàn chỉnh hơn của animation:
let start = Date.now(); // remember start time
let timer = setInterval(function() {
// how much time passed from the start?
let timePassed = Date.now() - start;
if (timePassed >= 2000) {
clearInterval(timer); // finish the animation after 2 seconds
return;
}
// draw the animation at the moment timePassed
draw(timePassed);
}, 20);
// as timePassed goes from 0 to 2000
// left gets values from 0px to 400px
function draw(timePassed) {
train.style.left = timePassed / 5 + 'px';
}
2. Sử dụng requestAnimationFrame
Hãy tưởng tượng chúng ta có nhiều animations đang chạy đồng thời.
Nếu chúng ta chạy chúng riêng biệt, thì mặc dù mỗi cái có setInterval(..., 20)
, nhưng trình duyệt sẽ phải vẽ lại nhiều hơn so với mỗi 20ms
.
Đó là vì chúng có thời gian bắt đầu khác nhau, vì vậy “mỗi 20ms” khác nhau giữa các animations khác nhau. Các khoảng thời gian không được căn chỉnh. Vì vậy, chúng ta sẽ có nhiều lần chạy độc lập trong 20ms
.
Nói cách khác, điều này:
setInterval(function() {
animate1();
animate2();
animate3();
}, 20)
…Nhẹ hơn so với ba cuộc gọi độc lập:
setInterval(animate1, 20); // independent animations
setInterval(animate2, 20); // in different places of the script
setInterval(animate3, 20);
Những lần vẽ lại độc lập này nên được nhóm lại với nhau, để giúp trình duyệt vẽ lại dễ dàng hơn, do đó giảm tải CPU và làm cho animation trông mượt mà hơn.
Còn một điều nữa cần lưu ý. Đôi khi CPU bị quá tải, hoặc có những lý do khác để vẽ lại ít thường xuyên hơn (như khi tab trình duyệt bị ẩn), vì vậy chúng ta thực sự không nên chạy nó mỗi 20ms
.
Nhưng làm sao chúng ta biết được điều đó trong JavaScript? Có một đặc tả Animation timing cung cấp hàm requestAnimationFrame
. Nó giải quyết tất cả các vấn đề này và thậm chí nhiều hơn thế.
Cú pháp:
let requestId = requestAnimationFrame(callback)
Điều này lên lịch cho hàm callback
chạy vào thời gian gần nhất khi trình duyệt muốn thực hiện animation.
Nếu chúng ta thực hiện các thay đổi trong các phần tử trong callback
, thì chúng sẽ được nhóm lại với các callback requestAnimationFrame
khác và với các animations CSS. Vì vậy, sẽ chỉ có một lần tính toán lại hình học và vẽ lại thay vì nhiều lần.
Giá trị trả về requestId
có thể được sử dụng để hủy cuộc gọi:
// cancel the scheduled execution of callback
cancelAnimationFrame(requestId);
callback
nhận một đối số — thời gian đã trôi qua từ khi trang bắt đầu tải tính bằng milliseconds. Thời gian này cũng có thể được lấy bằng cách gọi performance.now().
Thông thường callback
chạy rất nhanh, trừ khi CPU bị quá tải hoặc pin laptop gần hết, hoặc có lý do nào đó khác.
Đoạn mã dưới đây hiển thị thời gian giữa 10 lần chạy đầu tiên của requestAnimationFrame
. Thông thường là 10-20ms:
run height=40 refresh
<script>
let prev = performance.now();
let times = 0;
requestAnimationFrame(function measure(time) {
document.body.insertAdjacentHTML("beforeEnd", Math.floor(time - prev) + " ");
prev = time;
if (times++ < 10) requestAnimationFrame(measure);
})
</script>
3. Animation có cấu trúc
Bây giờ chúng ta có thể tạo ra một hàm animation đa dụng hơn dựa trên requestAnimationFrame
:
function animate({timing, draw, duration}) {
let start = performance.now();
requestAnimationFrame(function animate(time) {
// timeFraction goes from 0 to 1
let timeFraction = (time - start) / duration;
if (timeFraction > 1) timeFraction = 1;
// calculate the current animation state
let progress = timing(timeFraction)
draw(progress); // draw it
if (timeFraction < 1) {
requestAnimationFrame(animate);
}
});
}
Hàm animate
chấp nhận 3 tham số mà về cơ bản mô tả animation:
duration
: Tổng thời gian của animation. Ví dụ, 1000
.timing(timeFraction)
: Hàm thời gian, giống như thuộc tính CSS transition-timing-function
nhận phần thời gian đã trôi qua (0
lúc bắt đầu, 1
khi kết thúc) và trả về độ hoàn thành của animation (như y
trên đường cong Bezier).
Ví dụ, một hàm tuyến tính có nghĩa là animation diễn ra đều đặn với tốc độ như nhau:
function linear(timeFraction) {
return timeFraction;
}
Đồ thị của nó:
Điều này giống như transition-timing-function: linear
. Dưới đây là các biến thể thú vị hơn.
draw(progress)
: Hàm nhận trạng thái hoàn thành của animation và vẽ nó. Giá trị progress=0
biểu thị trạng thái bắt đầu của animation, và progress=1
— trạng thái kết thúc.
Đây là hàm thực sự vẽ ra animation.
Nó có thể di chuyển phần tử:
function draw(progress) {
train.style.left = progress + 'px';
}
…Hoặc làm bất kỳ điều gì khác, chúng ta có thể animate bất cứ thứ gì, theo bất kỳ cách nào.
Hãy animate thuộc tính width
của phần tử từ 0
đến 100%
bằng cách sử dụng hàm của chúng ta.
Đoạn mã cho nó:
animate({
duration: 1000,
timing(timeFraction) {
return timeFraction;
},
draw(progress) {
elem.style.width = progress * 100 + '%';
}
});
Không giống như CSS animation, chúng ta có thể tạo bất kỳ hàm thời gian và hàm vẽ nào ở đây. Hàm thời gian không bị giới hạn bởi các đường cong Bezier. Và draw
có thể vượt ra ngoài các thuộc tính, tạo ra các phần tử mới như trong animation pháo hoa hoặc gì đó tương tự.
4. Hàm thời gian
Chúng ta đã thấy hàm thời gian đơn giản nhất, hàm thời gian tuyến tính ở trên.
Hãy xem thêm một số hàm thời gian khác. Chúng ta sẽ thử các animations chuyển động với các hàm thời gian khác nhau để xem chúng hoạt động như thế nào.
Lũy thừa của n
Nếu chúng ta muốn tăng tốc độ animation, chúng ta có thể sử dụng progress
với lũy thừa n
.
Ví dụ, một đường cong parabol:
function quad(timeFraction) {
return Math.pow(timeFraction, 2)
}
Đồ thị:
Xem trong hành động (nhấp để kích hoạt):
…Hoặc đường cong bậc ba hoặc thậm chí lớn hơn n
. Tăng lũy thừa làm cho nó tăng tốc nhanh hơn.
Đây là đồ thị cho progress
với lũy thừa 5
:
Test thử trên demo:
Cung tròn(The arc)
Hàm:
function circ(timeFraction) {
return 1 - Math.sin(Math.acos(timeFraction));
}
Đồ thị:
Trở lại: bắn cung(Back: bow shooting)
Hàm này thực hiện “bắn cung”. Đầu tiên, chúng ta “kéo dây cung”, và sau đó “bắn”.
Không giống như các hàm trước, hàm này phụ thuộc vào một tham số bổ sung x
, “hệ số đàn hồi”. Khoảng cách “kéo dây cung” được xác định bởi nó.
Mã:
function back(x, timeFraction) {
return Math.pow(timeFraction, 2) * ((x + 1) * timeFraction - x)
}
Đồ thị cho x = 1.5
:
Để thực hiện animation, chúng ta sử dụng nó với một giá trị cụ thể của x
. Ví dụ với x = 1.5
:
Bật nảy(Bounce)
Hãy tưởng tượng chúng ta đang thả một quả bóng. Nó rơi xuống, sau đó bật lại vài lần và dừng lại.
Hàm bounce
làm điều tương tự, nhưng theo thứ tự ngược lại: “bật nảy” bắt đầu ngay lập tức. Nó sử dụng một vài hệ số đặc biệt cho điều đó:
function bounce(timeFraction) {
for (let a = 0, b = 1; 1; a += b, b /= 2) {
if (timeFraction >= (7 - 4 * a) / 11) {
return -Math.pow((11 - 6 * a - 11 * timeFraction) / 4, 2) + Math.pow(b, 2)
}
}
}
Animation đàn hồi
Một hàm “đàn hồi” khác chấp nhận một tham số bổ sung x
cho “phạm vi ban đầu”.
function elastic(x, timeFraction) {
return Math.pow(2, 10 * (timeFraction - 1)) * Math.cos(20 * Math.PI * x / 3 * timeFraction)
}
Đồ thị cho x=1.5
:
Trong hành động với x=1.5
:
5. Đảo ngược: ease*
Vậy là chúng ta có một bộ sưu tập các hàm thời gian. Ứng dụng trực tiếp của chúng được gọi là “easeIn”.
Đôi khi chúng ta cần hiển thị animation theo thứ tự ngược lại. Điều đó được thực hiện bằng cách chuyển đổi “easeOut”.
easeOut
Ở chế độ “easeOut”, hàm timing
được đặt vào một bộ bao timingEaseOut
:
timingEaseOut(timeFraction) = 1 - timing(1 - timeFraction)
Nói cách khác, chúng ta có một hàm “chuyển đổi” makeEaseOut
lấy một hàm thời gian “thông thường” và trả về một bộ bao quanh nó:
// accepts a timing function, returns the transformed variant
function makeEaseOut(timing) {
return function(timeFraction) {
return 1 - timing(1 - timeFraction);
}
}
Ví dụ, chúng ta có thể lấy hàm bounce
đã mô tả ở trên và áp dụng nó:
let bounceEaseOut = makeEaseOut(bounce);
Khi đó, sự bật nảy sẽ không ở đầu, mà ở cuối animation. Trông thậm chí còn đẹp hơn:
Ở đây chúng ta có thể thấy cách chuyển đổi thay đổi hành vi của hàm:
Nếu có một hiệu ứng animation ở đầu, như bật nảy — nó sẽ được hiển thị ở cuối.
Trong đồ thị trên, bật nảy thông thường có màu đỏ, và easeOut bounce có màu xanh lam.
– Bật nảy thông thường — đối tượng bật ở dưới cùng, sau đó cuối cùng nhảy lên đỉnh một cách đột ngột.
– Sau easeOut
— nó sẽ nhảy lên đỉnh trước, sau đó bật nảy ở đó.
easeInOut
Chúng ta cũng có thể hiển thị hiệu ứng cả ở đầu và cuối của animation. Chuyển đổi này được gọi là “easeInOut”.
Với hàm thời gian cho trước, chúng ta tính toán trạng thái animation như sau:
if (timeFraction <= 0.5) { // first half of the animation
return timing(2 * timeFraction) / 2;
} else { // second half of the animation
return (2 - timing(2 * (1 - timeFraction))) / 2;
}
Code:
function makeEaseInOut(timing) {
return function(timeFraction) {
if (timeFraction < .5)
return timing(2 * timeFraction) / 2;
else
return (2 - timing(2 * (1 - timeFraction))) / 2;
}
}
bounceEaseInOut = makeEaseInOut(bounce);
Trong hành động, bounceEaseInOut
:
Demo
Chuyển đổi “easeInOut” kết hợp hai đồ thị thành một: easeIn
(thông thường) cho nửa đầu của animation và easeOut
(đảo ngược) — cho phần thứ hai.
Hiệu ứng này rõ ràng khi chúng ta so sánh các đồ thị của easeIn
, easeOut
và easeInOut
của hàm thời gian circ
:
– Đỏ là biến thể thông thường của circ
(easeIn
).
– Xanh lá — easeOut
.
– Xanh dương — easeInOut
.
Như chúng ta có thể thấy, đồ thị của nửa đầu animation là easeIn
thu nhỏ, và nửa sau là easeOut
thu nhỏ. Kết quả là, animation bắt đầu và kết thúc với cùng một hiệu ứng.
6. “Draw” thú vị hơn
Thay vì di chuyển phần tử, chúng ta có thể làm điều gì đó khác. Tất cả những gì chúng ta cần là viết hàm draw
phù hợp.
Đây là văn bản “bật nảy” đang được nhập sẳn rồi:
7. Tóm tắt
Đối với các animation mà CSS không xử lý tốt, hoặc những cái cần kiểm soát chặt chẽ, JavaScript có thể giúp. Các animation bằng JavaScript nên được thực hiện thông qua requestAnimationFrame
. Phương thức tích hợp sẵn này cho phép thiết lập một hàm callback để chạy khi trình duyệt chuẩn bị vẽ lại. Thông thường điều đó xảy ra rất sớm, nhưng thời gian chính xác phụ thuộc vào trình duyệt.
Khi trang đang ở chế độ nền, không có bất kỳ lần vẽ lại nào, vì vậy callback sẽ không chạy: animation sẽ bị tạm dừng và không tiêu tốn tài nguyên. Điều đó thật tuyệt vời.
Đây là hàm trợ giúp animate
để thiết lập hầu hết các animation:
function animate({timing, draw, duration}) {
let start = performance.now();
requestAnimationFrame(function animate(time) {
// timeFraction goes from 0 to 1
let timeFraction = (time - start) / duration;
if (timeFraction > 1) timeFraction = 1;
// calculate the current animation state
let progress = timing(timeFraction);
draw(progress); // draw it
if (timeFraction < 1) {
requestAnimationFrame(animate);
}
});
}
Tùy chọn:
– duration
— tổng thời gian animation tính bằng ms.
– timing
— hàm tính toán tiến trình animation. Nhận một phân số thời gian từ 0 đến 1, trả về tiến trình animation, thường từ 0 đến 1.
– draw
— hàm để vẽ animation.
Chắc chắn chúng ta có thể cải thiện nó, thêm nhiều tính năng, nhưng animation bằng JavaScript không phải là thứ được sử dụng hàng ngày. Chúng được dùng để làm điều gì đó thú vị và không chuẩn. Vì vậy, bạn nên thêm các tính năng mà bạn cần khi bạn cần chúng.
Animation bằng JavaScript có thể sử dụng bất kỳ hàm thời gian nào. Chúng tôi đã đề cập đến nhiều ví dụ và biến thể để làm cho chúng đa dạng hơn. Không giống như CSS, chúng ta không bị giới hạn bởi các đường cong Bezier ở đây.
Điều tương tự cũng áp dụng cho draw
: chúng ta có thể animate bất kỳ thứ gì, không chỉ các thuộc tính CSS.
Cảm ơn bạn đã theo dõi bài viết trên Cafedev về JavaScript animations. Chúng tôi hy vọng bạn đã có cái nhìn rõ hơn về cách sử dụng các kỹ thuật như `setInterval` và `requestAnimationFrame` để tạo ra những hiệu ứng động ấn tượng trên trang web của mình. Với những kiến thức này, bạn có thể nâng cao khả năng tương tác của trang web và mang đến trải nghiệm người dùng tốt hơn. Hãy tiếp tục khám phá và áp dụng những kỹ thuật mới để làm cho dự án của bạn trở nên nổi bật hơn!
Tham khảo thêm: MIỄN PHÍ 100% | Series tự học Javascrypt chi tiết, dễ hiểu từ cơ bản tới nâng cao + Full Bài Tập thực hành nâng cao trình dev
Các nguồn kiến thức MIỄN PHÍ VÔ GIÁ từ cafedev tại đây
Nếu bạn thấy hay và hữu ích, bạn có thể tham gia các kênh sau của CafeDev để nhận được nhiều hơn nữa:
Chào thân ái và quyết thắng!