กรณีศึกษา - การทดลองของ Google I/O 2013

เกริ่นนำ

เราได้พัฒนาชุดการทดลองและเกมที่เน้นอุปกรณ์เคลื่อนที่เป็นอันดับแรก โดยมุ่งเน้นการโต้ตอบการสัมผัส เสียงจาก Generative และความสนุกสนานในการค้นพบ เพื่อสร้างความน่าสนใจให้กับนักพัฒนาซอฟต์แวร์ในเว็บไซต์ Google I/O 2013 ก่อนที่จะเปิดให้ลงทะเบียนเข้าร่วมการประชุม ประสบการณ์แบบอินเทอร์แอกทีฟนี้ได้รับแรงบันดาลใจมาจากศักยภาพของโค้ดและพลังในการเล่น โดยเริ่มจากเสียงง่ายๆ อย่าง "I" และ "O" เมื่อคุณแตะโลโก้ I/O ใหม่

การเคลื่อนไหวแบบออร์แกนิก

เราตัดสินใจใช้ภาพเคลื่อนไหว I และ O ในเอฟเฟ็กต์ทั่วไปที่เคลื่อนไหวไปมาซึ่งไม่ค่อยพบในการโต้ตอบของ HTML5 การโทรหาหมายเลขต่างๆ ทำให้รู้สึกว่าสนุกสนานและตอบสนองต้องใช้เวลาเล็กน้อย

ตัวอย่างรหัส Bouncy Physics

เพื่อให้ได้ผลลัพธ์นี้ เราใช้การจำลองทางฟิสิกส์ง่ายๆ บนชุดจุดที่แสดงขอบของรูปร่างทั้ง 2 รูป เมื่อแตะรูปร่างใดรูปร่างหนึ่ง ระบบจะเร่งจุดทั้งหมดออกจากตำแหน่งของการแตะ ปุ่มเหล่านี้จะยืดออกและเลื่อนออกก่อนที่จะดึงกลับคืนมา

ในอินสแตนซ์ แต่ละจุดจะได้รับปริมาณการเร่งแบบสุ่มและ "ตีกลับ" เพื่อไม่ให้เคลื่อนไหวอย่างสม่ำเสมอ ดังที่คุณเห็นในโค้ดนี้

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

จากนั้นเมื่อแตะ ตัวเร่งจะถูกเร่งออกจากตำแหน่งของการแตะ โดยใช้โค้ดต่อไปนี้

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

ในขั้นตอนสุดท้าย อนุภาคทุกอนุภาคจะถูกลดความเร็วลงในทุกเฟรมและค่อยๆ กลับสู่สภาวะสมดุลด้วยวิธีการต่อไปนี้ในโค้ด

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

การสาธิตภาพเคลื่อนไหวแบบออร์แกนิก

โหมดหน้าแรกของ I/O พร้อมให้คุณเล่น นอกจากนี้ เรายังมีตัวเลือกเพิ่มเติมมากมายสำหรับการติดตั้งใช้งาน หากคุณเปิด "แสดงจุด" คุณจะเห็นจุดแต่ละจุดที่การจำลองทางฟิสิกส์และแรงกระทำ

การปรับผิวใหม่

เมื่อพอใจกับการเคลื่อนไหวในโหมดบ้านแล้ว เราก็ต้องการใช้เอฟเฟกต์เดียวกันนี้สำหรับโหมดย้อนยุค 2 โหมด ได้แก่ Eightbit และ Ascii

เพื่อให้การปรับโฉมครั้งนี้ เราใช้แคนวาสเดียวกันจากโหมดหน้าแรก และใช้ข้อมูลพิกเซลในการสร้างเอฟเฟกต์แต่ละอย่าง วิธีนี้ชวนให้นึกถึงโหมด Fragment ของ OpenGL ที่ซึ่งแต่ละพิกเซลในฉากได้รับการตรวจสอบและปรับแต่ง เรามาเจาะลึกเรื่องนี้กัน

ตัวอย่างโค้ด "Shader" สำหรับ Canvas

ผู้ใช้ Pixel บน Canvas จะอ่านได้โดยใช้เมธอด getImageData อาร์เรย์ที่แสดงผลมีค่า 4 ค่าต่อพิกเซลที่แสดงถึงค่า RGBA แต่ละพิกเซล พิกเซลเหล่านี้จะต่อกันเป็นโครงสร้างคล้ายอาร์เรย์ขนาดใหญ่ ตัวอย่างเช่น ผืนผ้าใบ 2x2 จะมีรายการ 4 พิกเซลและ 16 รายการในอาร์เรย์ imageData

ผืนผ้าใบของเรากลายเป็นแบบเต็มหน้าจอ ดังนั้นหากเราสมมติว่าหน้าจอมีขนาด 1024x768 (เหมือนใน iPad) อาร์เรย์ก็จะมีรายการถึง 3,145,728 รายการ เนื่องจากเป็นภาพเคลื่อนไหว อาร์เรย์ทั้งหมดจึงจะได้รับการอัปเดต 60 ครั้งต่อวินาที เครื่องมือ JavaScript สมัยใหม่สามารถจัดการการวนซ้ำและการดำเนินการกับข้อมูลจำนวนมากนี้อย่างรวดเร็วพอที่จะทำให้อัตราเฟรมสอดคล้องกัน (เคล็ดลับ: อย่าลองบันทึกข้อมูลนั้นลงในแผงควบคุมสำหรับนักพัฒนาซอฟต์แวร์ เนื่องจากจะทำให้เบราว์เซอร์ของคุณทำการ Crawl ได้ช้าลง หรือเบราว์เซอร์ขัดข้องอย่างสมบูรณ์)

ต่อไปนี้คือวิธีที่โหมด 8bit จะอ่านแคนวาสของโหมดอยู่บ้านและเพิ่มพูนพิกเซลเพื่อสร้างเอฟเฟกต์บล็อก

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

การสาธิต 8bit Shader

ด้านล่าง เราตัดการวางซ้อน Eightbit และดูภาพเคลื่อนไหวต้นฉบับที่อยู่ด้านล่าง ตัวเลือก "ปิดหน้าจอ" จะแสดงเอฟเฟกต์แปลกๆ ที่เราพบโดยการสุ่มตัวอย่างพิกเซลต้นฉบับอย่างไม่ถูกต้อง สุดท้ายแล้วเราใช้โมเดลนี้เป็น Easter Egg ที่ "ปรับเปลี่ยนตามอุปกรณ์" เมื่อมีการปรับขนาดโหมด 8 บิตเป็นสัดส่วนภาพที่ไม่น่าจะเกิน ขอให้มีความสุขกับอุบัติเหตุ

การประกอบผ้าใบ

การรวมขั้นตอนการแสดงผลและมาสก์เข้าด้วยกันเป็นสิ่งที่น่าทึ่งมาก เราสร้างเมตาบอล 2 มิติซึ่งกำหนดให้ลูกบอลแต่ละลูกมีการไล่ระดับสีแบบรัศมีของตัวเอง และการไล่ระดับสีเหล่านั้นต้องผสมผสานเข้าด้วยกันในที่ที่ลูกบอลทับซ้อนกัน (ดูได้ในการสาธิตด้านล่าง)

เราใช้ภาพพิมพ์แคนวาส 2 ใบแยกกันเพื่อให้บรรลุเป้าหมาย ผืนผ้าใบแรกจะคำนวณและวาดรูปร่างเมตาบอล ผืนผ้าใบที่สองจะวาดการไล่ระดับสีแบบรัศมีที่ตำแหน่งลูกบอลแต่ละตำแหน่ง จากนั้นรูปร่างจะมาสก์การไล่ระดับสี และเราจะแสดงผลผลลัพธ์สุดท้าย

ตัวอย่างโค้ดการผสม

โค้ดที่ทำให้ทุกอย่างเกิดขึ้นมีดังนี้

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

จากนั้นจัดพื้นที่สำหรับมาสก์และวาด ดังนี้

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

บทสรุป

เทคนิคที่หลากหลายที่เรานำมาใช้และเทคโนโลยีที่เราใช้ (เช่น Canvas, SVG, CSS Animation, JS Animation, Web Audio และอื่นๆ) ทำให้โครงการนี้พัฒนาได้อย่างสนุกสนาน

และยังมีอะไรให้สำรวจอีกมากมายนอกเหนือจากที่คุณเห็นที่นี่ แตะโลโก้ I/O ไปเรื่อยๆ แล้วลำดับที่ถูกต้องจะปลดล็อกการทดลองย่อย เกม ภาพสวยๆ และเมนูอาหารเช้า เราขอแนะนำให้คุณลองใช้บนสมาร์ทโฟนหรือแท็บเล็ตของคุณเพื่อประสบการณ์ที่ดีที่สุด

นี่คือชุดค่าผสมเพื่อเริ่มต้น: O-I-I-I-I-I-I-I ลองใช้เลยที่ google.com/io

โอเพนซอร์ส

เราได้ใช้โค้ด Apache 2.0 แบบโอเพนซอร์ส คุณสามารถอ่านใน GitHub ได้ที่ http://github.com/Instrument/google-io-2013

เครดิต

ผู้พัฒนา:

  • โธมัส เรย์โนลด์
  • ไบรอัน เฮฟเตอร์
  • สเตฟานี่ แฮทเชอร์
  • พอล ฟาร์นิง

นักออกแบบ:

  • แดน เชคเตอร์
  • เขียวเสจ
  • ไคล์ เบ็ค

ผู้ผลิต:

  • อามี่ ปาสกาล
  • แอนเดรีย เนลสัน