מבוא
באביב האחרון (2010) התעניינתי בתמיכה ההולכת וגדלה ב-HTML5 ובטכנולוגיות קשורות. באותו זמן, חבר ואני היינו מתחרים זה בזה בתחרויות פיתוח משחקים של שבועיים כדי לשפר את מיומנויות התכנות והפיתוח שלנו, וגם כדי להפוך למציאות את רעיונות המשחקים שאנחנו מציעים כל הזמן אחד לשני. לכן, באופן טבעי התחלתי לשלב רכיבי HTML5 בתכנים שהגשתי לתחרויות כדי להבין טוב יותר איך הם פועלים ולבצע פעולות שהיו כמעט בלתי אפשריות באמצעות מפרטי HTML קודמים.
מבין התכונות החדשות הרבות ב-HTML5, התמיכה ההולכת וגדלה בתג canvas נתנה לי הזדמנות מרגשת להטמיע אמנות אינטראקטיבית באמצעות JavaScript. ההזדמנות הזו הובילה אותי לנסות להטמיע משחק פאזל שנקרא עכשיו Entanglement. כבר יצרתי אב טיפוס באמצעות הגב של המשבצות של Settlers of Catan, כך שאפשר להשתמש בו כתוכנית כללית. יש שלושה חלקים חיוניים ליצירת המשבצת המשושה בבד הקנבס של HTML5 למשחק באינטרנט: ציור המשושה, ציור הנתיבים וסיבוב המשבצת. בהמשך מפורט איך השגתי כל אחת מהמטרות האלה בצורתן הנוכחית.
שרטוט המשושה
בגרסה המקורית של Entanglement, השתמשתי בכמה שיטות ציור על לוח כדי לצייר את משושה, אבל בגרסה הנוכחית של המשחק נעשה שימוש ב-drawImage()
כדי לצייר טקסטורות שנחתכו מגיליון ספרייטים.

שילבתי את התמונות בקובץ אחד כדי שיהיו רק בקשה אחת לשרת במקום עשר בקשות במקרה הזה. כדי לצייר משושה שנבחר על הלוח, קודם צריך לאסוף את הכלים שלנו: הלוח, ההקשר והתמונה.
כדי ליצור לוח, כל מה שצריך הוא תג ה-canvas במסמך ה-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';
עכשיו, אחרי שקיבלנו את שלושת הרכיבים, אפשר להשתמש ב-ctx.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 מהקצוות לאורך כל קצה, ו-1/2 מהקצה מפריד ביניהן, כך:

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

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

עכשיו, כשיש לנו את הקואורדינטות, אנחנו מוכנים להתחיל לצייר. נתחיל מחדש עם ctx.beginPath() ואז נעבור לנקודת הקצה הראשונה באמצעות:
ctx.moveTo(pointX1,pointY1);
לאחר מכן אפשר לצייר את הקו עצמו באמצעות ctx.bezierCurveTo() באופן הבא:
ctx.bezierCurveTo(controlX1, controlY1, controlX2, controlY2, pointX2, pointY2);
אנחנו רוצים שהקו יהיה עם גבול יפה, ולכן נצייר את הנתיב הזה פעמיים, תוך שימוש ברוחב ובצבע שונים בכל פעם. הצבע יוגדר באמצעות המאפיין ctx.strokeStyle והרוחב יוגדר באמצעות ctx.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 נקודות הקצה האחרות וגם את נקודות הבקרה התואמות של עקומת הבייזאר, נוכל לחזור על השלבים שלמעלה וליצור משבצת שנראית בערך כך:

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