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.

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';
}

Nhấp để xem bản demo:

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,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.

Nhấp vào phần tử để xem demo:

Đ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):

Link demo.


…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:

Link demo

Cung tròn(The arc)

Hàm:

function circ(timeFraction) {
  return 1 - Math.sin(Math.acos(timeFraction));
}

Đồ thị:


Link demo

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:

Demo

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)
    }
  }
}

Bạn test thử trong demo

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:

Link Demo

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:

Link demo


Ở đâ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 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, easeOuteaseInOut 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ươngeaseInOut.
Như chúng ta có thể thấy, đồ thị của nửa đầu animation 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:

Code demo

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!

Đăng ký kênh youtube để ủng hộ Cafedev nha các bạn, Thanks you!