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