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

توماس رينولدز
توماس رينولدز

مقدمة

ولزيادة اهتمام المطوّرين بموقع مؤتمر Google I/O لعام 2013 الإلكتروني قبل افتتاح المؤتمر، طوّرنا سلسلة من الألعاب والألعاب التي تركّز على الأجهزة الجوّالة أولاً وتركّز على التفاعلات باللمس والصوت التوليدي ومتعة الاستكشاف. تبدأ هذه التجربة التفاعلية بأصوات بسيطة مثل "I" و "O" عند النقر على شعار I/O الجديد، وتستند إلى قدرات الرموز وقوة اللعب.

حركة عضوية

قررنا تنفيذ الرسوم المتحركة 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 حيث يتم فحص كل بكسل من المشهد ومعالجته. دعونا نتعمق أكثر في هذا.

مثال على رمز "Shader" في لوحة الرسم

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

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

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

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 Shader

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

تركيب على اللوحة

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

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

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

إليك التعليمة البرمجية التي تؤدي إلى حدوث كل شيء:

// 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 المفتوح المصدر. يمكنك العثور عليه على جيت هب لدينا على: http://github.com/Instrument/google-io-2013.

المساهمون

المطورون:

  • توماس رينولدز
  • براين هيفتر
  • ستيفاني هاتشر
  • بول فارنينغ

المصممون:

  • دان شيشتر
  • بني غامق
  • كايل بيك

المنتجون:

  • إيمي باسكال
  • أندريا نيلسون