+19

[Avenger 4: ENDGAME] Làm hiệu ứng búng tay bay màu THANOS bằng Javascript nào

SPOILER ALERT!!!

Nếu bạn chưa xem AVENGERS 4: ENDGAME thì bạn nên cân nhắc trước khi đọc bài viết này, vì nội dung bài viết sẽ có spoil về nội dung phim. Còn nếu bạn đã từng xem rồi thì hôm nay một lần nữa mình xin được tái hiện lại kết quả của trận chiến trong Hồi Kết vừa ra mắt ngày 26 tháng 4 vừa qua bằng... javascript 😎

Trước ngày ra mắt ENDGAME không lâu, mình phát hiện Google có bí mật làm một chức năng hết sức thú vị, mô phỏng lại hiệu ứng "búng tay" (finger snap) của Thanos như trong Infinity War và xoá sổ 50% số kết quả tìm thấy.

Mình thấy khá thú vị với trò đó của Google, vậy là cũng thử tìm tòi xem làm 1 cái tương tự. Nhưng lần này, Thanos đã là chuyện cũ rồi, nếu ai đã xem phần 4 rồi đều biết, cuối phim Iron-Man sẽ là người búng tay, cái búng tay không chỉ thổi bay Thanos cùng quân đoàn của hắn mà còn lấy đi cả mạng sống của Tony 😢

Đây là demo của mình LIVE DEMO: https://trandaison.github.io/

Concept

Nếu inspect code của Google ra xem bạn sẽ dễ dàng nhận thấy Google chuyển đổi các HTML element sang Canvas, tạo ra nhiều lớp canvas chồng lên nhau, cách lớp canvas này sẽ chứa 1 vài phần nhỏ của hình gốc, sau đó kết hợp giữa hiệu ứng xoay (rorate) và transform vị trí cho đến khi chúng bị mờ đi đến mức 0.

Vậy chúng ta cũng sẽ làm với ý tưởng tương tự.

Step 1: Convert HTML Element sang Canvas

Hên là đã có một thư viện hơn 16k sao cho phép làm điều này html2canvas. Bây giờ chỉ cần dựng HTML lên để có được không gian của bãi chiến trường. Trong End Game Tony mang bộ mark 85 bạn nhớ tìm hình cho giống nhé LOL =))

Còn đây là the battle field của mình

Với html2canvas bạn chỉ cần pass element cần chuyển vào trong hàm, hàm sẽ return về một promise với đối số là canvas được tạo.

html2canvas($(".content")[0]).then(canvas => {
    //capture all div data as image
    ctx = canvas.getContext("2d");
    var imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
    var pixelArr = imageData.data;
});

NOTE: Bạn lưu ý element truyền vào phải là HTML element khác thẻ img nhé.

Step 2: Tạo ra các lớp canvas

Sau bước một chúng ta đã có một mảng các pixel của hình ảnh. Việc còn lại chỉ đơn giản là phân số lượng pixel đó vào vài canvas khác nhau là xong.

Tuy nhiên để cho đẹp và hiệu ứng trông sẽ thật hơn nếu bạn làm nó "bay màu" từ đầu xuống chân, hoặc từ chân lên đầu, thay vì tan biến 1 cách ngẫu nhiên ko có tôn ti trật tự.

Nghĩa là chúng ta không thể chỉ đơn giản quất hàm Math.random được. Cần phải đánh trọng số cho việc phân tán các điểm ảnh.

Về cơ bản thì chúng ta sẽ tăng xác suất xuất hiện các điểm ảnh nằm ở phần đầu vào các canvases nằm trên. Tăng xác suất xuất hiện của các điểm ảnh ở phần thân vào các canvases nằm giữa, và tương tự cho phần chân.

Đồ thị biểu diễn nó đại khái như này

Để làm được điều này thì cũng đã có 1 thư viện hỗ trợ - chance.js. Bạn xem thêm docs của nó nếu có hứng thú nhé, docs cũng dài với nhiều phết đấy.

Đây là hàm trả về mảng các điểm ảnh sau khi đã đánh trọng số.

function weightedRandomDistrib(peak) {
  var prob = [], seq = [];
  for(let i=0;i<canvasCount;i++) {
    prob.push(Math.pow(canvasCount-Math.abs(peak-i),3));
    seq.push(i);
  }
  return chance.weighted(seq, prob);
}

Tiếp theo là tạo ra các lớp canvas cho Thanos từ mảng trên rồi append vào document (nhớ là chồng lên cái ảnh gốc để ko bị lộ, position: absolute; các thứ vào nhé)

//put pixel info to imageDataArray (Weighted Distributed)
for (let i = 0; i < pixelArr.length; i+=4) {
  //find the highest probability canvas the pixel should be in
  let p = Math.floor((i/pixelArr.length) *canvasCount);
  let a = imageDataArray[weightedRandomDistrib(p)];
  a[i] = pixelArr[i];
  a[i+1] = pixelArr[i+1];
  a[i+2] = pixelArr[i+2];
  a[i+3] = pixelArr[i+3]; 
}

//create canvas for each imageData and append to target element
for (let i = 0; i < canvasCount; i++) {
  let c = newCanvasFromImageData(imageDataArray[i], canvas.width, canvas.height);
  c.classList.add("dust");
  $(".wrapper").append(c);
}

Đây là các lớp canvas theo thứ tự sau khi được tạo ra

Step 3: Animation

Bước này chỉ cần thêm tí animation cho nó chuyển động các lớp canvas hạt bụi trên kia, như kiểu đang hấp hối bay màu =))

Lúc này bạn cần ẩn thằng ảnh gốc đi nhé.

//clear all children except the canvas
$(".content").children().not(".dust").fadeOut(3500);

Sau đó lặp từng cái canvas, lần lượt thêm vào 3 animations:

  • Blur: Có blur vào mới tạo ra được sự mượt mà khi chuyển động, để chuyển động ko bị "cứng".
  • Transform: Transform cả vị trí lẫn xoay ảnh, để đẩy các điểm ảnh ra xa dần.
  • Fade out: Cho điểm ảnh mờ dần đến 0.
//apply animation
$(".dust").each( function(index){
  animateBlur($(this),0.8,800);
  setTimeout(() => {
    animateTransform($(this),100,-100,chance.integer({ min: -15, max: 15 }),800+(110*index));
  }, 70*index); 
  //remove the canvas from DOM tree when faded
  $(this).delay(70*index).fadeOut((110*index)+800,"easeInQuint",()=> {$( this ).remove();});
});

jQuery không hỗ trợ hiệ ứng blur, check phần final code mình đính kèm nếu bạn có hứng thú nhé.

Nhớ rằng class dust là lớp ảnh chứa các điểm ảnh dùng cho bay màu, phải đặt chồng lên nhau nhé.

.dust {
  position: absolute;
}

Lời kết

Final source code mình đặt ở đây, mình thực hiện trên VueJS, nên nếu bạn có ý định chạy thử thì vào fork repo này về https://github.com/trandaison/fingerSnap

  • Chạy yarn install để cài các package.
  • Chạy yarn serve để chạy server node và vào localhost:8080 để xem.

Một số điểm mong các bạn lưu ý nếu có đọc qua source code.

  • Một số code đã được thay đổi cho phù hợp với cái demo của mình =))
  • Code js và css mình cũng làm vội cho có chứ đang lởm lắm, các bạn đừng để bụng.

Tham khảo từ tutorial tại: https://www.youtube.com/watch?v=fM791m4A_Pk


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí