מקרה לדוגמה - ניסוי של Google I/O לשנת 2013

Thomas Reynolds
Thomas Reynolds

מבוא

כדי לעורר עניין בקרב מפתחים באתר של 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, שבו כל פיקסל בסצנה נבדק ומטופל. נמשיך לדבר על זה.

דוגמה לקוד של 'Shader' ב-Canvas

אפשר לקרוא פיקסלים ב-Canvas באמצעות השיטה getImageData. המערך המוחזר מכיל 4 ערכים לכל פיקסל שמייצגים את ערך ה-RGBA של כל פיקסל. הפיקסלים האלה מחוברים זה לזה במבנה עצום שנראה כמו מערך. לדוגמה, לקנבס בגודל 2x2 יהיו 4 פיקסלים ו-16 רשומות במערך imageData שלו.

הלוח שלנו הוא מסך מלא, כך שאם נניח שהמסך הוא בגודל 1024x768 (כמו ב-iPad), למערך יהיו 3,145,728 רשומות. מכיוון שמדובר באנימציה, כל המערך הזה מתעדכן 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 ורואים את האנימציה המקורית שמתחתיה. האפשרות 'מסך שחור' תציג לכם אפקט מוזר שגילינו בטעות כשדגמנו את הפיקסלים המקוריים בצורה שגויה. בסופו של דבר השתמשנו בו כביצה 'רספונסיבית' לפסח כשמשנים את הגודל של מצב Eightbit ליחסי גובה-רוחב לא סבירים. תאונה משמחת!

שילוב תמונות ב-Canvas

מדהים מה אפשר להשיג על ידי שילוב של כמה שלבי רינדור ומסכות. יצרנו מטא-כדור 2D, שבו לכל כדור צריך להיות שיפוע רדיאלי משלו, והשיפועים האלה צריכים להתמזג יחד במקומות שבהם הכדורים חופפים. (אפשר לראות זאת בהדגמה שבהמשך).

כדי לעשות זאת, השתמשנו בשני קנבסים נפרדים. הלוח הראשון מחשב ומשרטט את צורת המטא-כדור. בבד הציור השני מצוירים מעברי צבע רדיאליים בכל מיקום של כדור. לאחר מכן הצורה מסתירה את העקומות והמערכת מבצעת עיבוד (רנדור) של הפלט הסופי.

דוגמה לקוד של קומפוזיציה

זה הקוד שמפעיל את כל התהליך:

// 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.

זיכויים

מפתחים:

  • תומאס ריינולדס
  • בריאן הפטיר (Brian Hefter)
  • Stefanie Hatcher
  • פול פארנינג

מעצבים:

  • דן שצ'טר (Dan Schechter)
  • ירקרק-חום
  • Kyle Beck

מפיקים:

  • Amie Pascal
  • אנדראה נלסון