Nghiên cứu điển hình – Thử nghiệm Google I/O năm 2013

Thomas Reynolds
Thomas Reynolds

Giới thiệu

Để thu hút sự quan tâm của nhà phát triển trên trang web Google I/O 2013 trước khi bắt đầu đăng ký hội nghị, chúng tôi đã phát triển một loạt thử nghiệm và trò chơi ưu tiên thiết bị di động, tập trung vào hoạt động tương tác chạm, âm thanh tạo sinh và niềm vui khám phá. Lấy cảm hứng từ tiềm năng của mã và khả năng chơi, trải nghiệm tương tác này bắt đầu bằng các âm thanh đơn giản của "I" và "O" khi bạn nhấn vào biểu trưng I/O mới.

Chuyển động tự nhiên

Chúng tôi đã quyết định triển khai các ảnh động I và O theo hiệu ứng tự nhiên, linh hoạt, thường không thấy trong các hoạt động tương tác HTML5. Bạn có thể gặp phải các lựa chọn để cảm thấy thú vị và mất chút thời gian để phản ứng.

Ví dụ về mã Bouncy Vật lý

Để có được hiệu ứng này, chúng ta đã sử dụng một hoạt động mô phỏng thực tế đơn giản trên một chuỗi các điểm đại diện cho cạnh của hai hình dạng. Khi một trong hai hình dạng được nhấn, tất cả các điểm đều được tăng tốc từ vị trí nhấn. Chúng kéo giãn và di chuyển ra trước khi chúng được kéo vào lại.

Khi tạo thực thể, mỗi điểm nhận được một mức tăng tốc ngẫu nhiên và phục hồi "độ nảy" nên chúng không đồng nhất, như bạn có thể thấy trong mã này:

this.paperO_['vectors'] = [];

// Add an array of vector points and properties to the object.
for (var i = 0; i < this.paperO_['segments'].length; i++) {
  var point = this.paperO_['segments'][i]['point']['clone']();
  point = point['subtract'](this.oCenter);

  point['velocity'] = 0;
  point['acceleration'] = Math.random() * 5 + 10;
  point['bounce'] = Math.random() * 0.1 + 1.05;

  this.paperO_['vectors'].push(point);
}

Sau đó, khi được nhấn, các mô-đun này được tăng tốc ra khỏi vị trí của lần nhấn bằng cách sử dụng mã sau đây:

for (var i = 0; i < path['vectors'].length; i++) {
  var point = path['vectors'][i];
  var vector;
  var distance;

  if (path === this.paperO_) {
    vector = point['add'](this.oCenter);
    vector = vector['subtract'](clickPoint);
    distance = Math.max(0, this.oRad - vector['length']);
  } else {
    vector = point['add'](this.iCenter);
    vector = vector['subtract'](clickPoint);
    distance = Math.max(0, this.iWidth - vector['length']);
  }

  point['length'] += Math.max(distance, 20);
  point['velocity'] += speed;
}

Cuối cùng, mọi phần tử đều được giảm tốc trên mọi khung hình và từ từ trở về trạng thái cân bằng theo phương pháp sau trong mã:

for (var i = 0; i < path['segments'].length; i++) {
  var point = path['vectors'][i];
  var tempPoint = new paper['Point'](this.iX, this.iY);

  if (path === this.paperO_) {
    point['velocity'] = ((this.oRad - point['length']) /
      point['acceleration'] + point['velocity']) / point['bounce'];
  } else {
    point['velocity'] = ((tempPoint['getDistance'](this.iCenter) -
      point['length']) / point['acceleration'] + point['velocity']) /
      point['bounce'];
  }

  point['length'] = Math.max(0, point['length'] + point['velocity']);
}

Bản minh hoạ chuyển động tự nhiên

Sau đây là chế độ màn hình chính I/O để bạn sử dụng. Chúng tôi cũng đã đưa ra một loạt các tuỳ chọn bổ sung trong cách triển khai này. Nếu bật chế độ "hiển thị điểm", bạn sẽ thấy từng điểm mà hoạt động mô phỏng thực tế và các lực tác động lên.

Chụp lại màn hình

Khi đã hài lòng với chuyển động của chế độ ở nhà, chúng tôi muốn sử dụng cùng hiệu ứng đó cho 2 chế độ cổ điển: 8bit và Ascii.

Để hoàn tất việc kết hợp lại này, chúng tôi đã sử dụng cùng một canvas ở chế độ ở nhà và sử dụng dữ liệu pixel để tạo từng hiệu ứng trong số hai hiệu ứng này. Phương pháp này gợi nhớ đến chương trình đổ bóng mảnh OpenGL, trong đó mỗi pixel của cảnh sẽ được kiểm tra và chỉnh sửa. Hãy cùng tìm hiểu kỹ hơn về vấn đề này.

Ví dụ về mã "Shader" của Canvas

Bạn có thể đọc pixel trên Canvas bằng phương thức getImageData. Mảng được trả về chứa 4 giá trị trên mỗi pixel đại diện cho mỗi giá trị RGBA của pixel. Các pixel này được xâu chuỗi với nhau trong một cấu trúc lớn giống như mảng. Ví dụ: một canvas 2x2 sẽ có 4 pixel và 16 mục nhập trong mảng imageData.

Canvas của chúng ta ở chế độ toàn màn hình, vì vậy nếu chúng ta giả vờ màn hình là 1024x768 (như trên iPad), thì mảng có 3.145.728 mục nhập. Vì đây là ảnh động nên toàn bộ mảng này được cập nhật 60 lần trong một giây. Các công cụ JavaScript hiện đại có thể xử lý việc lặp lại và hành động dựa trên nhiều dữ liệu này đủ nhanh để giữ tốc độ khung hình nhất quán. (Mẹo: không thử ghi nhật ký dữ liệu đó vào Play Console, vì việc này sẽ làm chậm quá trình thu thập dữ liệu của trình duyệt hoặc khiến trình duyệt gặp sự cố.)

Dưới đây là cách chế độ 8 bit đọc canvas chế độ ở nhà và làm nổ các pixel để tạo hiệu ứng chặn:

var pixelData = pctx.getImageData(0, 0, sourceCanvas.width, sourceCanvas.height);

// tctx is the Target Context for the output Canvas element
tctx.clearRect(0, 0, targetCanvas.width + 1, targetCanvas.height + 1);

var size = ~~(this.width_ * 0.0625);

if (this.height_ * 6 < this.width_) {
 size /= 8;
}

var increment = Math.min(Math.round(size * 80) / 4, 980);

for (i = 0; i < pixelData.data.length; i += increment) {
  if (pixelData.data[i + 3] !== 0) {
    var r = pixelData.data[i];
    var g = pixelData.data[i + 1];
    var b = pixelData.data[i + 2];
    var pixel = Math.ceil(i / 4);
    var x = pixel % this.width_;
    var y = Math.floor(pixel / this.width_);

    var color = 'rgba(' + r + ', ' + g + ', ' + b + ', 1)';

    tctx.fillStyle = color;

    /**
     * The ~~ operator is a micro-optimization to round a number down
     * without using Math.floor. Math.floor has to look up the prototype
     * tree on every invocation, but ~~ is a direct bitwise operation.
     */
    tctx.fillRect(x - ~~(size / 2), y - ~~(size / 2), size, size);
  }
}

Bản minh hoạ chương trình đổ bóng 8 bit

Dưới đây, chúng ta bỏ lớp phủ 8bit và xem ảnh động gốc bên dưới. Tuỳ chọn "đóng màn hình" sẽ cho bạn thấy một hiệu ứng lạ mà chúng tôi gặp phải do việc lấy mẫu không chính xác các pixel nguồn. Cuối cùng, chúng tôi đã sử dụng tính năng này làm chi tiết thú vị "thích ứng" khi chế độ 8bit được đổi kích thước thành tỷ lệ khung hình hiếm gặp. Chúc bạn tai nạn vui vẻ!

Kết hợp Canvas

Bạn có thể đạt được những kết quả xuất sắc bằng cách kết hợp nhiều bước kết xuất và mặt nạ. Chúng tôi xây dựng một siêu bóng dữ liệu 2D yêu cầu mỗi quả bóng có độ dốc dạng hình tròn riêng và các quả bóng đó phải được kết hợp với nhau ở nơi các quả bóng chồng lên nhau. (Bạn có thể xem điều này trong bản minh hoạ bên dưới.)

Để làm việc này, chúng tôi đã sử dụng hai canvas riêng biệt. Canvas đầu tiên sẽ tính toán và vẽ hình dạng metaball. Canvas thứ hai vẽ các hiệu ứng chuyển màu dạng hình tròn tại mỗi vị trí bóng. Sau đó, hình dạng sẽ hiện các lớp chuyển màu và chúng ta kết xuất đầu ra cuối cùng.

Ví dụ về mã kết hợp

Dưới đây là mã giúp mọi việc xảy ra:

// Loop through every ball and draw it and its gradient.
for (var i = 0; i < this.ballCount_; i++) {
  var target = this.world_.particles[i];

  // Set the size of the ball radial gradients.
  this.gradSize_ = target.radius * 4;

  this.gctx_.translate(target.pos.x - this.gradSize_,
    target.pos.y - this.gradSize_);

  var radGrad = this.gctx_.createRadialGradient(this.gradSize_,
    this.gradSize_, 0, this.gradSize_, this.gradSize_, this.gradSize_);

  radGrad.addColorStop(0, target['color'] + '1)');
  radGrad.addColorStop(1, target['color'] + '0)');

  this.gctx_.fillStyle = radGrad;
  this.gctx_.fillRect(0, 0, this.gradSize_ * 4, this.gradSize_ * 4);
};

Sau đó, thiết lập canvas để tạo mặt nạ và vẽ:

// Make the ball canvas the source of the mask.
this.pctx_.globalCompositeOperation = 'source-atop';

// Draw the ball canvas onto the gradient canvas to complete the mask.
this.pctx_.drawImage(this.gcanvas_, 0, 0);
this.ctx_.drawImage(this.paperCanvas_, 0, 0);

Kết luận

Nhiều kỹ thuật chúng tôi đã áp dụng cũng như các công nghệ đã triển khai (chẳng hạn như Canvas, SVG, CSS Animation, JS Animation, Web Audio, v.v.) giúp việc phát triển dự án trở nên vô cùng thú vị.

Còn có nhiều điều khác để khám phá hơn những gì bạn thấy ở đây. Hãy tiếp tục nhấn vào biểu trưng I/O và trình tự chính xác sẽ giúp bạn có thêm nhiều thử nghiệm nhỏ, trò chơi, hình ảnh sinh động và thậm chí là một số món ăn sáng. Bạn nên dùng thử tính năng này trên điện thoại thông minh hoặc máy tính bảng để có trải nghiệm tốt nhất.

Dưới đây là kết hợp để giúp bạn bắt đầu: O-I-I-I-I-I-I-I. Dùng thử ngay: google.com/io

Nguồn mở

Chúng tôi đã cung cấp giấy phép Apache 2.0 cho mã nguồn mở của mình. Bạn có thể tìm thấy mã này trên GitHub của chúng tôi tại: http://github.com/Instrument/google-io-2013.

Ghi công

Nhà phát triển:

  • Thomas Reynolds
  • Brian Hefter
  • Máy ấp trứng Stefanie
  • Paul Farning

Nhà thiết kế:

  • Dan Schechter
  • Màu nâu xám
  • Kyle Beck

Nhà sản xuất:

  • Amie Pascal
  • Nghệ sĩ Andrea xuống