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

บทนำ

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

Organic Motion

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

ตัวอย่างโค้ดฟิสิกส์แบบเด้ง

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

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

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 โหมด ได้แก่ 8 บิตและ ASCII

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

ตัวอย่างโค้ด "Shader" ของ Canvas

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

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

วิธีที่โหมด Eightbit จะอ่าน Canvas ของโหมด Home และจะเพิ่มพิกเซลให้มีเอฟเฟกต์แบบบล็อก ดังนี้

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

การสาธิต Shader แบบ 8 บิต

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

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

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

เราใช้ผืนผ้าใบ 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, ภาพเคลื่อนไหว JS, เสียงบนเว็บ ฯลฯ) ทําให้การพัฒนาโปรเจ็กต์นี้สนุกมาก

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

ตัวอย่างการกดแป้นเพื่อเริ่มต้นใช้งานคือ O-I-I-I-I-I-I ลองใช้เลยที่ google.com/io

โอเพนซอร์ส

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

เครดิต

ผู้พัฒนา:

  • Thomas Reynolds
  • ไบรอัน เฮฟเตอร์
  • Stefanie Hatcher
  • พอล ฟาร์นิง

นักออกแบบ

  • Dan Schechter
  • สีน้ำตาล Sage
  • Kyle Beck

ผู้ผลิต:

  • Amie Pascal
  • Andrea Nelson