Chủ Nhật, 06/10/2019 | 00:00 GMT+7

Ảnh động với API Canvas - Phần 3: Trọng lực và kết xuất động


Trong Phần 2 của loạt bài này, ta đã tạo ra một quả bóng sẽ chảy xung quanh màn hình và đổi màu khi nó va chạm với đường viền. Bây giờ ta sẽ sử dụng những gì ta đã học để tạo ra hình ảnh động mưa này hiển thị động các server với hiệu ứng hạt khi mỗi server chạm đáy canvas của ta .

bản mẫu

Vì ta sẽ làm việc ở gần cuối màn hình, ta nên ẩn bất kỳ thanh cuộn ngang nào có overflow: hidden , và ta sẽ làm tối nó một chút để bớt nhức mắt hơn một chút.

index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
    <meta http-equiv="X-UA-Compatible" content="ie=edge"/>
    <title>HTML Canvas</title>
    <style>
      body {
        overflow: hidden;
        background-color: #1a202c;
      }
    </style>
  </head>
  <body>

    <canvas></canvas>

  </body>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/dat-gui/0.7.6/dat.gui.min.js"></script>
  <script src="./canvas.js"></script>
</html>
canvas.js
// Utilities
const randomNum = (min, max) => Math.floor(Math.random() * (max - min + 1) + min);
const randomColor = colors => colors[Math.floor(Math.random() * colors.length)];

// Get canvas element
const canvas = document.querySelector('canvas');
const c = canvas.getContext('2d');

// Make canvas fullscreen
canvas.width = innerWidth;
canvas.height = innerHeight;
addEventListener('resize', () => {
  canvas.width = innerWidth;
  canvas.height = innerHeight;
});

// Control Panel
const gui = new dat.GUI();

const controls = {
  count: 0,
  velocity: 0,
};

gui.add(controls, 'dx', 0, 10);
gui.add(controls, 'dy', 0, 10);

// New Object
class Ball {
  constructor(x, y, radius, color) {
    this.x = x;
    this.y = y;
    this.radius = radius;
    this.color = color;
  }
};

Ball.prototype.draw = function () {
  c.beginPath();
  c.fillStyle = this.color;
  c.arc(this.x, this.y, this.radius, 0, Math.PI * 2, false);
  c.fill();
  c.closePath();
};

Ball.prototype.update = function () {
  this.x += controls.dx;
  this.y += -controls.dy;
  this.draw();
};

const ball = new Ball(innerWidth / 2, innerHeight / 2, 50, 'red');

// Handle changes
const animate = () => {
  requestAnimationFrame(animate);

  c.clearRect(0, 0, canvas.width, canvas.height);

  ball.update();
};

animate();

Rơi vãi

Hãy bắt đầu bằng cách làm cho các server chính của ta hoạt động. Ta chỉ cần lưu trữ tất cả các biến cho mỗi drop dưới dạng một đối tượng, vẽ một đường trên màn hình và thêm một số giá trị vào vị trí y khi nào update được chạy để làm cho nó di chuyển xuống dưới.

canvas.js
class Drop {
  constructor(x, y, dy, thickness, length, color) {
    this.x = x;
    this.y = y;
    this.dy = dy;
    this.thickness = thickness;
    this.length = length;
    this.color = color;
  }
};

Drop.prototype.draw = function () {
  c.beginPath();
  c.strokeStyle = this.color;
  c.lineWidth = this.thickness;
  c.moveTo(this.x, this.y);
  c.lineTo(this.x, this.y - this.length);
  c.stroke();
  c.closePath();
}

Drop.prototype.update = function () {
  this.y += this.dy;

  this.draw();
}

Hãy kết xuất một cái vào giữa màn hình canvas để xem nó có hoạt động không.

const drop = new Drop(innerWidth / 2, innerHeight / 2, 2, 5, 30, 'red');

const animate = () => {
  requestAnimationFrame(animate);
  c.clearRect(0, 0, canvas.width, canvas.height);

  drop.update();
};

animate();

thả đơn

Ticker

Điều đó thật tuyệt, nhưng ta cần nhiều kết xuất hơn ở phía trên. Để có nhiều server , ta chỉ có thể tạo một mảng, sử dụng vòng lặp for để gán các giá trị ngẫu nhiên cho từng mục trước khi đẩy chúng vào mảng và sử dụng forEach để cập nhật từng mục đó trên mỗi khung. Nhưng chỉ sử dụng vòng lặp for sẽ chỉ hiển thị các server một lần, tất cả sẽ di chuyển qua canvas của ta ra khỏi trang web. Vì vậy, ta phải sáng tạo một chút để liên tục thêm các server mới trong khi loại bỏ tất cả các server di chuyển bên dưới màn hình của ta .

Để làm điều này, ta sẽ tạo một mã đánh dấu sẽ đếm theo từng khung trong mỗi khung, mỗi khi nó chia hết hoàn toàn cho một số nào đó, ta sẽ thêm một server mới vào mảng. Dù số nào ta chia nó sẽ kiểm soát tần suất các server mới được hiển thị. Để loại bỏ chúng và tiết kiệm sức mạnh xử lý, ta sẽ chỉ tách chúng ra khỏi mảng khi chúng ở dưới cùng.

Sử dụng toán tử modulo ( % ), ta có thể chia một số và kiểm tra xem phần còn lại có bằng 0. Vì vậy, nó càng cao thì mức giảm mới sẽ ít thường xuyên hơn.

Trong khi ta ở đây, hãy cung cấp cho họ một số màu sắc. Tôi thấy rằng việc sử dụng các giá trị khác nhau của cùng một màu, cùng với độ dày và chiều dài ngẫu nhiên, sẽ giúp tạo ra ảo giác về độ sâu nào đó. Tôi khuyên bạn nên xem Kuler để biết bảng màu của bạn.

canvas.js
const colors = [ '#9C4AFF', '#8C43E6', '#7638C2', '#5E2C99', '#492378'];

let drops = [];
let ticker = 0;
const animate = () => {
  requestAnimationFrame(animate);
  // Try using the 'residue' effect from Part 2
  // c.fillStyle = 'rgba(33, 33, 33, .3)'; //Lower opacity creates a longer tail
  // c.fillRect(0, 0, canvas.width, canvas.height);
  c.clearRect(0, 0, canvas.width, canvas.height);

  drops.forEach((drop, index) => {
    drop.update();
    if (drop.y >= canvas.height) drops.splice(index, 1);
  });

  // Timing between drops
  ticker++;
  let count = controls.count === 0 ? 0 : randomNum(controls.count + 5, controls.count);
  if (ticker % count == 0) {
    const x = randomNum(0, innerWidth);
    const y = 0;
    const dy = controls.velocity === 0 ? 0 : randomNum(controls.velocity, controls.velocity + 10);
    const thickness = randomNum(3, 5);
    const length = randomNum(20, 50);

    drops.push(new Drop(x, y, dy, thickness, length, randomColor(colors)));
  };
};

Server và trọng lực

Để tạo ra hiệu ứng giật gân khi chúng chạm đất, ta cần một số hạt nhỏ hơn sẽ có hiệu ứng giống trọng lực, phóng ra từ server chính. Lớp Server của ta sẽ khá giống với Drop chính của ta , với một số khác biệt vì các server sẽ là hình tròn thay vì đường thẳng.

canvas.js
class Server {
  constructor(x, y, dx, dy, radius, color) {
    this.x = x;
    this.y = y;
    this.dx = dx;
    this.dy = dy;
    this.radius = radius;
    this.color = color;
    this.gravity = .1;
  }
};

Server.prototype.draw = function () {
  c.beginPath();
  c.arc(this.x, this.y, this.radius, 0, Math.PI * 2, false);
  c.fillStyle = this.color;
  c.fill();
  c.closePath();
};

Điều chính ta muốn lo lắng là lực hấp dẫn của ta . Lực hấp dẫn khiến chuyển động đi xuống của ta tăng lên, vì vậy ta sẽ muốn thêm điều này vào dy của ta trên phương thức update . Vì vậy, khi ta tạo ra nhiều server di chuyển lên, với giá trị dy âm và thêm giá trị trọng lực của ta vào mỗi khung hình, nó sẽ chậm lại, đảo ngược hướng và tăng tốc cho đến khi nó bị xóa khỏi khung vẽ của ta . Tôi đã làm một ví dụ đơn giản hơn chỉ với các server nhỏ, bạn có thể thử nghiệm tại đây .

Server.prototype.update = function () {
  this.dy += this.gravity;
  this.y += this.dy;
  this.x += this.dx;

  this.draw();
};

Và ta sẽ cập nhật và xóa chúng giống như những server chính của ta .

let server = [];
const animate = () => {
  server.forEach((server, index) => {
    server.update();
    if (server.y >= canvas.height) server.splice(index, 1);
  });
};

Splash

Việc thêm các phần tử của ta thực sự rất đơn giản, ta chỉ có thể sử dụng vòng lặp for để tạo chúng với vị trí server chính và chuyển vào một số giá trị ngẫu nhiên cho phần còn lại. Hãy cũng thêm một số lực hấp dẫn vào server của ta để làm cho chúng rơi thực tế hơn một chút.

canvas.js
class Drop {
  constructor(x, y, dy, thickness, length, color) {
    this.x = x;
    this.y = y;
    this.dy = dy;
    this.thickness = thickness;
    this.color = color;
    this.length = length;
    this.gravity = .4;
  }
};

Drop.prototype.update = function () {
  // Stops drops if velocity controller is set to 0
  if (this.dy > 0) this.dy += this.gravity;
  this.y += this.dy;

  // It runs splash over the whole length of the drop, to we'll narrow it down to the end.
  if (this.y > canvas.height - 100) this.splash(this.x, this.y + (this.length * 2));

  this.draw();
}

Drop.prototype.splash = function (x, y) {
  for (let i = 0; i < 5; i++) {
    const dx = randomNum(-3, 3);
    const dy = randomNum(-1, -5);
    const radius = randomNum(1, 3);

    server.push(new Server(x, y, dx, dy, radius, randomColor(colors)));
  };
};

Kết luận

Mặc dù vẫn còn rất nhiều điều anh ấy học được về HTML canvas, nhưng hy vọng loạt bài ngắn này là một phần giới thiệu đủ nhẹ nhàng về các khả năng của nó. Hầu hết các trang web đều giống nhau về nhiều mặt nhưng khả năng tạo hoạt ảnh tùy chỉnh mang lại sự độc đáo mà các công cụ tạo trang web tự động phổ biến nhất không bao giờ có được.


Tags:

Các tin liên quan