מקרה לדוגמה - להסתבך עם בד ציור של HTML5

Derek Detweiler
Derek Detweiler

מבוא

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

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

ציור המשושה

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

גיליון Sprite של אריחים
גיליון Sprite של אריחים

שילבתי את התמונות לקובץ אחד, כך שהייתה רק בקשה אחת לשרת במקום, במקרה הזה, עשר. כדי לצייר משושה על לוח הציור, תחילה עלינו לאסוף את הכלים שלנו יחד: קנבס, ההקשר והתמונה.

כדי ליצור קנבס, אנחנו צריכים רק את תג הקנבס במסמך ה-HTML שלנו, באופן הבא:

<canvas id="myCanvas"></canvas>

אני נותן לו מזהה כדי שנוכל להכניס אותו לסקריפט שלנו:

var cvs = document.getElementById('myCanvas');

שנית, אנחנו צריכים לקחת את ההקשר הדו-ממדי של הקנבס כדי להתחיל לצייר:

var ctx = cvs.getContext('2d');

לסיום, אנחנו צריכים את התמונה. במקרה שהשם שלו הוא "tiles.png" באותה תיקייה שבה נמצא דף האינטרנט שלנו, נוכל לקבל אותו כך:

var img = new Image();
img.src = 'tiles.png';

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

ctx.drawImage(img, sourceX, sourceY, sourceWidth, sourceHeight,
            destinationX, destinationY, destinationWidth, destinationHeight);

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

var cvs = document.getElementById('myCanvas');
var ctx = cvs.getContext('2d');
var img = new Image();
img.src = 'tiles.png';
var sourceX = 1200;
var sourceY = 0;
var sourceWidth = 400;
var sourceHeight = 346;
var destinationX = 0;
var destinationY = 0;
var destinationWidth = 400;
var destinationHeight = 346;
ctx.drawImage(img, sourceX, sourceY, sourceWidth, sourceHeight,
            destinationX, destinationY, destinationWidth, destinationHeight);

העתקנו בהצלחה חלק מהתמונה לקנבס והתוצאה:

אריח משושה
משבצת משושה

שרטוט נתיבים

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

נקודות קצה של קו במשבצת משושה
נקודות קצה (endpoint) של קו באריח משושה

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

נקודות בקרה על אריח משושה
נקודות בקרה במשבצת משושה

עכשיו אנחנו ממפים גם את נקודות הקצה וגם את נקודות הבקרה למישור קרטזי שתואם לתמונת הקנבס שלנו, ואנחנו מוכנים לחזור לקוד. כדי לפשט את העניינים, נתחיל בשורה אחת. נתחיל בשרטוט נתיב מנקודת הקצה השמאלית העליונה אל נקודת הקצה הימנית התחתונה. התמונה המשושה הקודמת שלנו היא 400x346, ולכן הסיומת של נקודת הקצה העליונה תהיה 150 פיקסלים לרוחב ו-0 פיקסלים למטה, מקוצר (150, 0). נקודת הבקרה שלה תהיה (150, 86). נקודת הקצה התחתונה היא (250, 346) עם נקודת בקרה של (250, 260):

קואורדינטות של עקומת בזייה הראשונה
קואורדינטות של עקומת בזייה הראשונה

כעת אנו מוכנים להתחיל לצייר, כאשר נקודות הציון שלנו בידינו. אנחנו נתחיל מחדש עם hl=iw&sjde

ctx.moveTo(pointX1,pointY1);

לאחר מכן נוכל לשרטט את הקו עצמו באמצעות בק.bezierCurveTo() באופן הבא:

ctx.bezierCurveTo(controlX1, controlY1, controlX2, controlY2, pointX2, pointY2);

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

var pointX1 = 150;
var pointY1 = 0;
var controlX1 = 150;
var controlY1 = 86;
var controlX2 = 250;
var controlY2 = 260;
var pointX2 = 250;
var pointY2 = 346;
ctx.beginPath();
ctx.moveTo(pointX1, pointY1);
ctx.bezierCurveTo(controlX1, controlY1, controlX2, controlY2, pointX2, pointY2);
ctx.lineWidth = 15;
ctx.strokeStyle = '#ffffff';
ctx.stroke();
ctx.lineWidth = 10;
ctx.strokeStyle = '#786c44';
ctx.stroke();

עכשיו יש לנו אריח משושה עם השורה הראשונה שמתפתלת לרוחב:

קו בודד על אריח משושה
קו יחיד על גבי אריח משושה

אם תזינו את הקואורדינטות של 10 נקודות הקצה האחרות, וגם את נקודות הבקרה של עקומת ה-bezier, נוכל לחזור על השלבים שלמעלה ועשויים ליצור אריח שנראה כך:

הושלם אריח משושה.
משבצת משושה הושלמה

סיבוב לוח הציור

ברגע שיש לנו את המשבצת שלנו, אנחנו רוצים להיות מסוגלים להפוך אותו כדי שנוכל לצעוד בדרכים שונות במשחק. כדי לעשות את זה באמצעות קנבס, אנחנו משתמשים ב-ctx.translate() וב-ctx.rotate(). אנחנו רוצים שהאריח יסתובב סביב המרכז, ולכן השלב הראשון הוא להעביר את נקודת ההתייחסות של הקנבס למרכז המשבצת המשושה. לשם כך אנחנו משתמשים:

ctx.translate(originX, originY);

כאשר originX יהיה חצי מהרוחב של האריח המשושה ו-originY יהיה חצי מהגובה, וכך נוכל:

var originX = 200;
var originY = 173;
ctx.translate(originX, originY);

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

ctx.rotate(Math.PI / 3);

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

var originX = 200;
var originY = 173;
ctx.translate(originX, originY);
ctx.rotate(Math.PI / 3);
ctx.translate(-originX, -originY);

הוספת התרגום והסיבוב שלעיל לפני קוד העיבוד תגרום לעיבוד של האריח המסובב:

אריח משושה מסובב
אריח משושה מסובב

סיכום

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

סימוכין לקוד

כל דוגמאות הקוד שצוינו למעלה משולבות למטה כחומר עזר:

var cvs = document.getElementById('myCanvas');
var ctx = cvs.getContext('2d');
var img = new Image();
img.src = 'tiles.png';

var originX = 200;
var originY = 173;
ctx.translate(originX, originY);
ctx.rotate(Math.PI / 3);
ctx.translate(-originX, -originY);

var sourceX = 1200;
var sourceY = 0;
var sourceWidth = 400;
var sourceHeight = 346;
var destinationX = 0;
var destinationY = 0;
var destinationWidth = 400;
var destinationHeight = 346;
ctx.drawImage(img, sourceX, sourceY, sourceWidth, sourceHeight,
            destinationX, destinationY, destinationWidth, destinationHeight);

ctx.beginPath();
var pointX1 = 150;
var pointY1 = 0;
var controlX1 = 150;
var controlY1 = 86;
var controlX2 = 250;
var controlY2 = 260;
var pointX2 = 250;
var pointY2 = 346;
ctx.moveTo(pointX1, pointY1);
ctx.bezierCurveTo(controlX1, controlY1, controlX2, controlY2, pointX2, pointY2);
ctx.lineWidth = 15;
ctx.strokeStyle = '#ffffff';
ctx.stroke();
ctx.lineWidth = 10;
ctx.strokeStyle = '#786c44';
ctx.stroke();

ctx.beginPath();
pointX1 = 250;
pointY1 = 0;
controlX1 = 250;
controlY1 = 86;
controlX2 = 150;
controlY2 = 86;
pointX2 = 75;
pointY2 = 43;
ctx.moveTo(pointX1, pointY1);
ctx.bezierCurveTo(controlX1, controlY1, controlX2, controlY2, pointX2, pointY2);
ctx.lineWidth = 15;
ctx.strokeStyle = '#ffffff';
ctx.stroke();
ctx.lineWidth = 10;
ctx.strokeStyle = '#786c44';
ctx.stroke();

ctx.beginPath();
pointX1 = 150;
pointY1 = 346;
controlX1 = 150;
controlY1 = 260;
controlX2 = 300;
controlY2 = 173;
pointX2 = 375;
pointY2 = 213;
ctx.moveTo(pointX1, pointY1);
ctx.bezierCurveTo(controlX1, controlY1, controlX2, controlY2, pointX2, pointY2);
ctx.lineWidth = 15;
ctx.strokeStyle = '#ffffff';
ctx.stroke();
ctx.lineWidth = 10;
ctx.strokeStyle = '#786c44';
ctx.stroke();

ctx.beginPath();
pointX1 = 325;
pointY1 = 43;
controlX1 = 250;
controlY1 = 86;
controlX2 = 300;
controlY2 = 173;
pointX2 = 375;
pointY2 = 130;
ctx.moveTo(pointX1, pointY1);
ctx.bezierCurveTo(controlX1, controlY1, controlX2, controlY2, pointX2, pointY2);
ctx.lineWidth = 15;
ctx.strokeStyle = '#ffffff';
ctx.stroke();
ctx.lineWidth = 10;
ctx.strokeStyle = '#786c44';
ctx.stroke();

ctx.beginPath();
pointX1 = 25;
pointY1 = 130;
controlX1 = 100;
controlY1 = 173;
controlX2 = 100;
controlY2 = 173;
pointX2 = 25;
pointY2 = 213;
ctx.moveTo(pointX1, pointY1);
ctx.bezierCurveTo(controlX1, controlY1, controlX2, controlY2, pointX2, pointY2);
ctx.lineWidth = 15;
ctx.strokeStyle = '#ffffff';
ctx.stroke();
ctx.lineWidth = 10;
ctx.strokeStyle = '#786c44';
ctx.stroke();

ctx.beginPath();
pointX1 = 325;
pointY1 = 303;
controlX1 = 250;
controlY1 = 260;
controlX2 = 150;
controlY2 = 260;
pointX2 = 75;
pointY2 = 303;
ctx.moveTo(pointX1, pointY1);
ctx.bezierCurveTo(controlX1, controlY1, controlX2, controlY2, pointX2, pointY2);
ctx.lineWidth = 15;
ctx.strokeStyle = '#ffffff';
ctx.stroke();
ctx.lineWidth = 10;
ctx.strokeStyle = '#786c44';
ctx.stroke();