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