دراسة حالة - تجربة Google I/O 2013

مقدمة

لجذب اهتمام المطوّرين على الموقع الإلكتروني لمؤتمر Google I/O لعام 2013 قبل فتح باب التسجيل في المؤتمر، طوّرنا سلسلة من التجارب والألعاب المخصّصة للأجهزة الجوّالة والتي تركّز على التفاعلات باللمس والصوت التوليدي ومتعتي الاستكشاف. مستوحاة من إمكانات الرموز البرمجية وقوة اللعب، تبدأ هذه التجربة التفاعلية بصوتَي "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" المنزلي الذي يمكنك اللعب به. لقد عرضنا أيضًا مجموعة من الخيارات الإضافية في عملية التنفيذ هذه. في حال تفعيل خيار "عرض النقاط"، ستظهر لك النقاط الفردية التي تؤثر فيها محاكاة الفيزياء والقوى.

تغيير المظهر

بعد أن اطمأنّا إلى أنّ حركة وضع "داخل المنزل" مناسبة، أردنا استخدام هذا التأثير نفسه في وضعَين قديمَين: Eightbit وAscii.

لإنجاز هذا التغيير، استخدمنا لوحة الرسم نفسها من وضع "داخل المنزل" ونستخدم بيانات البكسل لإنشاء كل من التأثيرَين. تذكّرنا هذه الطريقة ببرنامج OpenGL fragment shader الذي يتم فيه فحص كل بكسل من المشهد ومعالجته. لنطّلع على مزيد من التفاصيل.

مثال على رمز "تأثيرات الإضاءة" في لوحة الرسم

يمكن قراءة وحدات البكسل على لوحة باستخدام طريقة getImageData. يحتوي الصفيف المعروض على 4 قيم لكل بكسل تمثّل قيمة RGBA لكل بكسل. ويتم تجميع هذه البكسلات معًا في بنية ضخمة تشبه الصفيف. على سبيل المثال، تحتوي لوحة بحجم 2×2 على 4 بكسل و16 إدخالاً في مصفوفة imageData الخاصة بها.

اللوحة هي بملء الشاشة، لذا إذا افترضنا أنّ الشاشة بدقة 1024x768 (مثل جهاز iPad)، سيكون للصفيف 3,145,728 إدخال. وبما أنّ هذا تأثير متحرك، يتم تعديل هذه الصفيف بالكامل 60 مرة في الثانية. يمكن لمحرّكات JavaScript الحديثة معالجة تكرار هذه البيانات واتخاذ الإجراءات اللازمة بشأنها بسرعة كافية للحفاظ على اتساق معدل عرض اللقطات. (نصيحة: لا تحاول تسجيل هذه البيانات إلى وحدة تحكم المطوّرين، لأنّ ذلك سيؤدي إلى إبطاء المتصفِّح والزحف إليه أو إيقافه تمامًا).

وفي ما يلي طريقة قراءة وضع "الثمانية بتاتًا" للوحة الرسم في وضع "الصفحة الرئيسية" وتفجير وحدات البكسل للحصول على تأثير كتلي:

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

عرض توضيحي لتأثيرات الإضاءة الثنائية الثمانية

في ما يلي، أزلنا الصورة المتراكبة Eightbit وظهرت الصورة المتحركة الأصلية تحتها. سيعرض لك خيار "إيقاف الشاشة" تأثيرًا غريبًا صادفناه من خلال أخذ عيّنات غير صحيحة من وحدات البكسل المصدر. وانتهى بنا الأمر باستخدامه كمفاجأة مخفية "متجاوبة" عندما يتم تغيير حجم وضع "الثمانية بتات" إلى نِسب عرض إلى ارتفاع غير متوقّعة. حادث سعيد!

تركيب اللوحة

من المدهش ما يمكنك تحقيقه من خلال الجمع بين خطوات التقديم والعديد من الأقنعة. أنشأنا كرة ميتافيزيائية ثنائية الأبعاد تتطلّب أن يكون لكل كرة تدرج إشعاعي خاص بها وأن يتم دمج هذه التدرجات معًا عند تداخل الكرات. (يمكنك الاطّلاع على ذلك في العرض التوضيحي أدناه).

ولتحقيق ذلك، استخدمنا لوحتَين منفصلتَين. تحسب اللوحة الأولى شكل كرة الميتابول وترسمه. ترسم لوحة ثانية تدرجات شعاعية في كل موضع للكرة. ثم يخفي الشكل التدرجات ونعرض الناتج النهائي.

مثال على رمز تركيب

في ما يلي الرمز البرمجي الذي ينفِّذ كل الإجراءات:

// 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. جرِّب هذه الميزة الآن: google.com/io

برنامج مفتوح المصدر

لقد فتحنا مصدر ترخيص Apache 2.0 الخاص بالرموز. يمكنك العثور عليه على GitHub في: http://github.com/Instrument/google-io-2013.

المساهمون

المطورون:

  • توماس رينولدز
  • براين هيفتر
  • منى كمال
  • بول فارنينج

المصممون:

  • دان شيشتر
  • بني أخضر
  • كاميليا

المنتجون:

  • Amie Pascal
  • أندريا نيلسون