مطالعه موردی - آزمایش 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']);
}

دمو حرکت ارگانیک

در اینجا حالت خانه ورودی/خروجی برای بازی وجود دارد. ما همچنین مجموعه ای از گزینه های اضافی را در این پیاده سازی در معرض دید قرار داده ایم. اگر "نمایش نقاط" را روشن کنید، نقاط فردی را خواهید دید که شبیه سازی فیزیک و نیروها روی آنها عمل می کنند.

پوست اندازی مجدد

هنگامی که ما از حرکت حالت خانگی راضی بودیم، می‌خواستیم از همان جلوه برای دو حالت یکپارچهسازی با سیستمعامل استفاده کنیم: Eightbit و Ascii.

برای انجام این تغییر شکل، از همان بوم از حالت خانه استفاده کردیم و از داده های پیکسل برای تولید هر یک از دو افکت استفاده کردیم. این رویکرد یادآور یک Shader Fragment OpenGL است که در آن هر پیکسل صحنه بازرسی و دستکاری می شود. بیایید بیشتر به این موضوع بپردازیم.

نمونه کد بوم "Shader".

پیکسل های روی بوم را می توان با استفاده از روش getImageData خواند. آرایه برگشتی شامل 4 مقدار در هر پیکسل است که هر پیکسل مقدار RGBA را نشان می دهد. این پیکسل ها در یک ساختار آرایه مانند عظیم در کنار هم قرار گرفته اند. به عنوان مثال، یک بوم 2x2 دارای 4 پیکسل و 16 ورودی در آرایه imageData خود است.

بوم ما تمام صفحه است، بنابراین اگر وانمود کنیم صفحه نمایش 1024x768 است (مانند یک iPad)، آرایه دارای 3،145،728 ورودی است. از آنجا که این یک انیمیشن است، کل این آرایه 60 بار در ثانیه به روز می شود. موتورهای جاوا اسکریپت مدرن می توانند حلقه زدن و عمل بر روی این حجم از داده ها را به اندازه کافی سریع انجام دهند تا نرخ فریم ثابت بماند. (نکته: سعی نکنید آن داده‌ها را در کنسول توسعه‌دهنده ثبت کنید، زیرا باعث کاهش سرعت مرورگر شما می‌شود یا آن را کاملاً خراب می‌کند.)

در اینجا نحوه 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 Demo

در زیر، پوشش Eightbit را حذف می کنیم و انیمیشن اصلی را در زیر می بینیم. گزینه "kill screen" اثر عجیبی را به شما نشان می دهد که ما با نمونه برداری نادرست از پیکسل های منبع به آن برخورد کردیم. زمانی که حالت 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 ادامه دهید و دنباله‌های صحیح، آزمایش‌های کوچک، بازی‌ها، تصاویر تریپ و شاید حتی برخی از غذاهای صبحانه را باز می‌کنند. پیشنهاد می کنیم برای بهترین تجربه آن را روی گوشی هوشمند یا تبلت خود امتحان کنید.

در اینجا ترکیبی برای شروع شما وجود دارد: OIIIIIII. اکنون آن را امتحان کنید: google.com/io

متن باز

ما مجوز کد Apache 2.0 خود را منبع باز کرده ایم. می‌توانید آن را در Github ما در آدرس زیر پیدا کنید: http://github.com/Instrument/google-io-2013 .

وام

توسعه دهندگان:

  • توماس رینولدز
  • برایان هفتر
  • استفانی هچر
  • پل فارنینگ

طراحان:

  • دن شچتر
  • سیج براون
  • کایل بک

تهیه کنندگان:

  • امی پاسکال
  • آندره آ نلسون