מקרה לדוגמה - ניסוי של 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 של פיקסלים. הפיקסלים האלה קשורים זה לזה במבנה עצום דמוי מערך. לדוגמה, לקנבס בגודל 2x2 יהיו 4 פיקסלים ו-16 רשומות במערך imageData שלו.

הקנבס שלנו הוא במסך מלא, כך שאם נעמיד פנים שהמסך הוא בגודל 1024x768 (כמו ב-iPad), המערך יכלול 3,145,728 רשומות. מכיוון שמדובר באנימציה, המערך כולו מתעדכן 60 פעמים בשנייה. מנועי JavaScript מודרניים יכולים להתמודד עם לולאה ולפעול על פי כמות הנתונים הזו מהר מספיק כדי לשמור על עקביות של קצב הפריימים. (טיפ: אל תנסה לרשום את הנתונים האלה ב-Developer Console, כי זה יאט את הסריקה או יגרום לקריסה של הדפדפן).

כך מצב 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 ליחס גובה-רוחב בלתי סביר. תאונה שמחה!

איחוד קנבס

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

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

דוגמה לקוד מורכב

הנה הקוד שגורם לכל דבר:

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

זיכויים

מפתחים:

  • תומאס ריינולדס
  • בריאן הפטר
  • סטפני האצ'ר
  • פול פארנינג

מעצבים:

  • דן שכטר
  • מרווה חום
  • קייל בק

מפיקים:

  • אמיי פסקל
  • אנדריאה נלסון