บทนำ
เราได้พัฒนาชุดการทดลองและเกมที่เน้นอุปกรณ์เคลื่อนที่เป็นหลักเพื่อดึงดูดความสนใจของนักพัฒนาซอฟต์แวร์ในเว็บไซต์ 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