Loading Now

Cách làm hiệu ứng búng tay Thanos cực ngầu với JavaScript

Bạn đã bao giờ lên Google và search từ khóa “Thanos” để xem thử hiệu ứng tan biến khi búng tay ở góc phải màn hình?

Nếu bạn kiểm tra mã nguồn của Google, bạn sẽ thấy họ làm biến mất các link bằng cách với mỗi link họ sẽ tạo ra nhiều thẻ canvas và thực hiện các phép biến đổi trên các thẻ canvas này.

Với cách làm này, chúng ta sẽ tìm cách chuyển đổi các link thành các ảnh. Sau đó với mỗi pixel trong ảnh chúng ta sẽ chia nó về 1 canvas ngẫu nhiên (hiểu đơn giản là mỗi link sẽ có nhiều canvas và mỗi một pixel trong link này sẽ nằm trong một canvas nào đó). Cuối cùng là thêm hiệu ứng biến mất cho từng canvas và ẩn link ban đầu đi.

Vậy ý tưởng chỉ đơn giản là chuyển đổi các link thành các ảnh, chia các ảnh này thành các phần nhỏ và thực hiện thêm các animation vào các phần nhỏ này.

Bước một – Chuyển các thẻ HTML sang ảnh.

Dưới đây là một ví dụ mẫu:

<div class="content">
     <img src="person.png" height="600">
     <button id="start-btn">Snap!</button>
</div>

Để lấy được các pixel của một thẻ HTML trước hết chúng ta cần đưa được thẻ này về canvas object (sử dụng thư viện html2canvas) rồi sau đó từ canvas ta sẽ dùng các hàm có sẵn để lấy ra được các pixel. Ví dụ:

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;

Bước thứ hai – Chia các pixel vào các canvas.

Sau khi đã có được các pixel, ta sẽ cần phải chia chúng vào các canvas. Để hiệu ứng biến mất xảy ra từ trên xuống ta không thể chia đều các pixel cho các canvas bằng cách random các pixel vào các canvas thông thường, ví dụ để làm hiệu ứng biến mất chiếc hamburger từ trên xuống ta cần chia các pixel ở trên top vào các canvas ở đầu, các pixel ở phần dưới vào các canvas ở cuối và cuối cùng sẽ thực hiện biến mất cho từng canvas theo thứ tự và chiếc bánh sẽ trông như đang biến mất từ trên xuống.

Có thể thấy ta không thể làm bằng hàm Math.random thông thường mà phải sử dụng các thư viện random khác như chance.js. Nếu chưa biết cách dùng thư viện này, bạn có thể tham khảo một video hướng dẫn tại đây.

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 ta sẽ tạo ra các canvas từ các pixel và thêm thuộc tính class cho những canvas này.

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

Bước cuối cùng – Hiệu ứng động

Bước cuối cùng chính là thêm hiệu ứng vào. Đầu tiên chúng ta sẽ bắt đầu làm ẩn tất cả những thứ không thuộc class dust (ẩn miếng bánh và nút Snap!).

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

Sau đó, với mỗi khung canvas, chúng ta sẽ thêm ba hiệu ứng. Đầu tiên là mờ. Thứ hai là transform (rotate và translate) để di chuyển các pixel ra khỏi vị trí ban đầu. Hiệu ứng thứ ba là làm mờ dần các pixel.

//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();});
});

Phần khó là jQuery không hỗ trợ trực tiếp làm mờ hoặc chuyển đổi hình ảnh động nên chúng ta phải tự tạo một hàm cho chúng (Xem mã nguồn đầy đủ bên dưới)

Về code CSS thì không có gì khó và cũng không nhiều. Chỉ cần dùng flexbox để căn giữa mọi thứ. Điều duy nhất liên quan đến hiệu ứng trong CSS chính là sét vị trí cho các class dust là absolute. Điều này giúp cho các canvas ở một ví trí cố định.

.dust {
  position: absolute;
}

Trên đấy là ý tưởng để làm hiệu ứng biến mất của thanos, bạn có thể xem video và source code ở phía dưới.

Source code

HTML:

<!DOCTYPE html>
<html>
    <head>
    <meta http-equiv="content-type" content="text/html; charset=UTF-8" />
    <link rel="stylesheet" type="text/css" href="styles.css">
    <script src="html2canvas.min.js"></script>
    <script src="chance.min.js"></script>
    <script src="jquery-2.1.4.js"></script>
    <script src="jquery-ui-1.9.2.custom.min.js"></script>
    </head>
    <body>
      <div class="content">
        <img src="burger.png" height="600">
        <button id="start-btn">Snap!</button>
      </div>   
    <script> 
    
    var imageDataArray = [];
    var canvasCount = 35;
    $("#start-btn").click(function(){
      
      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;
        createBlankImageData(imageData);
        //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");
          $("body").append(c);
        }
        //clear all children except the canvas
        $(".content").children().not(".dust").fadeOut(3500);
        //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();});
        });
      });
    });
    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);
    }
    function animateBlur(elem,radius,duration) {
      var r =0;
      $({rad:0}).animate({rad:radius}, {
          duration: duration,
          easing: "easeOutQuad",
          step: function(now) {
            elem.css({
                  filter: 'blur(' + now + 'px)'
              });
          }
      });
    }
    function animateTransform(elem,sx,sy,angle,duration) {
      var td = tx = ty =0;
      $({x: 0, y:0, deg:0}).animate({x: sx, y:sy, deg:angle}, {
          duration: duration,
          easing: "easeInQuad",
          step: function(now, fx) {
            if (fx.prop == "x") 
              tx = now;
            else if (fx.prop == "y") 
              ty = now;
            else if (fx.prop == "deg") 
              td = now;
            elem.css({
                  transform: 'rotate(' + td + 'deg)' + 'translate(' + tx + 'px,'+ ty +'px)'
              });
          }
      });
    }
    function createBlankImageData(imageData) {
      for(let i=0;i<canvasCount;i++)
      {
        let arr = new Uint8ClampedArray(imageData.data);
        for (let j = 0; j < arr.length; j++) {
            arr[j] = 0;
        }
        imageDataArray.push(arr);
      }
    }
    function newCanvasFromImageData(imageDataArray ,w , h) {
      var canvas = document.createElement('canvas');
          canvas.width = w;
          canvas.height = h;
          tempCtx = canvas.getContext("2d");
          tempCtx.putImageData(new ImageData(imageDataArray, w , h), 0, 0);
          
      return canvas;
    }
    </script>
    </body>
</html>

CSS:

* {
  box-sizing: border-box;
}
body {
  margin: 0;
  min-height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
  background: #ddd;
}
.content {
  display: flex;
  align-items: center;
  flex-direction: column;
  background: #ddd;
}
#start-btn {
  font-size: 36px;
  padding: 20px 40px 20px 80px ;
  margin-top: 30px;
  border-radius: 10px;
  background:url("thanos-logo.png") white 15px no-repeat;
  background-size: 50px;
}
.dust {
  position: absolute;
}

Nguồn tham khảo: https://redstapler.co/thanos-snap-effect-javascript-tutorial/

Post Comment

Contact