กรณีศึกษา - ปัญหาเกี่ยวกับ Canvas ของ HTML5

บทนำ

เมื่อช่วงฤดูใบไม้ผลิที่ผ่านมา (2010) เราสนใจการรองรับ HTML5 และเทคโนโลยีที่เกี่ยวข้องที่เพิ่มขึ้นอย่างรวดเร็ว ตอนนั้นฉันและเพื่อนได้ท้าทายกันในการแข่งพัฒนาเกม 2 สัปดาห์เพื่อฝึกฝนทักษะการเขียนโปรแกรมและการพัฒนา รวมถึงทำให้ไอเดียเกมที่เราคิดขึ้นมาด้วยกันกลายเป็นจริง ด้วยเหตุนี้ เราจึงเริ่มใช้องค์ประกอบ HTML5 ในผลงานที่ส่งเข้าประกวดเพื่อให้เข้าใจวิธีการทำงานขององค์ประกอบเหล่านี้ได้ดียิ่งขึ้น และสามารถทําสิ่งต่างๆ ที่แทบจะเป็นไปไม่ได้เมื่อใช้ข้อกําหนด HTML เวอร์ชันเก่า

ฟีเจอร์ใหม่มากมายใน HTML5 ให้การรองรับแท็ก Canvas มากขึ้น ซึ่งเปิดโอกาสให้ฉันได้แสดงผลงานศิลปะแบบอินเทอร์แอกทีฟโดยใช้ JavaScript ซึ่งทำให้ฉันได้ลองใช้เกมปริศนาที่ตอนนี้เรียกว่า Entanglement เราได้สร้างต้นแบบโดยใช้ด้านหลังของไทล์ Settlers of Catan แล้ว ดังนั้นเมื่อใช้ต้นแบบนี้เป็นพิมพ์เขียว การสร้างไทล์หกเหลี่ยมบนผืนผ้าใบ HTML5 สำหรับการเล่นเกมบนเว็บจะประกอบไปด้วย 3 ส่วนสำคัญ ได้แก่ การวาดหกเหลี่ยม การวาดเส้นทาง และการหมุนไทล์ ต่อไปนี้เป็นรายละเอียดที่อธิบายวิธีที่ฉันทําสิ่งเหล่านี้ให้สำเร็จในรูปแบบปัจจุบัน

การวาดรูปหกเหลี่ยม

ใน Entanglement เวอร์ชันแรก เราใช้วิธีการวาดภาพแคนวาสหลายวิธีเพื่อวาดรูปหกเหลี่ยม แต่รูปแบบปัจจุบันของเกมใช้ drawImage() เพื่อวาดพื้นผิวที่ตัดมาจากสไปรท์ชีต

สไปรท์ชีตของไทล์
สไปรท์ชีตของไทล์

เราได้รวมรูปภาพเข้าด้วยกันเป็นไฟล์เดียวเพื่อให้มีคำขอไปยังเซิร์ฟเวอร์เพียงรายการเดียวแทนที่จะเป็น 10 รายการในกรณีนี้ หากต้องการวาดรูปหกเหลี่ยมที่เลือกลงในผืนผ้าใบ เราต้องรวบรวมเครื่องมือต่างๆ เข้าด้วยกันก่อน ได้แก่ ผืนผ้าใบ บริบท และรูปภาพ

หากต้องการสร้าง Canvas สิ่งที่เราต้องการมีเพียงแท็ก Canvas ในเอกสาร HTML ดังต่อไปนี้

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

เรากําหนดรหัสเพื่อให้ดึงข้อมูลนี้ไปไว้ในสคริปต์ได้

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

ขั้นตอนที่ 2 เราต้องรับบริบท 2 มิติสำหรับผืนผ้าใบเพื่อเริ่มวาดภาพ

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

สุดท้าย เราต้องการรูปภาพ หากชื่อไฟล์คือ "tiles.png" ในโฟลเดอร์เดียวกับหน้าเว็บ เราจะรับไฟล์ได้โดยทำดังนี้

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

เมื่อเรามีคอมโพเนนต์ 3 รายการแล้ว เราสามารถใช้ ctx.drawImage() เพื่อวาดหกเหลี่ยมรูปเดียวที่ต้องการจากสไปรท์ชีตไปยังผืนผ้าใบได้ ดังนี้

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

ในกรณีนี้ เราจะใช้รูปหกเหลี่ยมที่ 4 จากซ้ายในแถวบน นอกจากนี้ เราจะวาดรูปภาพนั้นลงในผืนผ้าที่มุมซ้ายบนโดยให้มีขนาดเท่ารูปภาพต้นฉบับ สมมติว่ารูปหกเหลี่ยมมีความกว้าง 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);

เราคัดลอกรูปภาพบางส่วนไปยังผืนผ้าใบเรียบร้อยแล้ว โดยผลลัพธ์ที่ได้คือ

กระเบื้องหกเหลี่ยม
กระเบื้องหกเหลี่ยม

การวาดเส้นทาง

เมื่อวาดหกเหลี่ยมบนผืนผ้าใบแล้ว เราต้องการวาดเส้น 2-3 เส้นบนหกเหลี่ยม ก่อนอื่น เราจะดูเรขาคณิตบางอย่างเกี่ยวกับการ์ดหกเหลี่ยม เราต้องการจุดสิ้นสุดของเส้น 2 จุดต่อด้าน โดยแต่ละจุดสิ้นสุดอยู่ห่างจากปลายตามขอบแต่ละด้าน 1/4 และอยู่ห่างจากกัน 1/2 ของขอบ ดังนี้

ปลายทางของเส้นบนไทล์หกเหลี่ยม
ปลายทางของเส้นบนการ์ดหกเหลี่ยม

เราต้องการเส้นโค้งที่สวยงามด้วย ดังนั้นหลังจากลองผิดลองถูกไปสักพัก เราพบว่าหากลากเส้นตั้งฉากจากขอบที่จุดสิ้นสุดแต่ละจุด จุดตัดจากคู่จุดสิ้นสุดแต่ละคู่รอบๆ มุมหนึ่งๆ ของหกเหลี่ยมจะเป็นจุดควบคุมของเส้นโค้ง Bezier ที่ยอดเยี่ยมสำหรับจุดสิ้นสุดนั้นๆ

จุดควบคุมบนไทล์หกเหลี่ยม
จุดควบคุมบนกระเบื้องหกเหลี่ยม

ตอนนี้เราจะแมปทั้งจุดปลายและจุดควบคุมไปยังระนาบพิกัดคาร์ทีเซียนซึ่งสอดคล้องกับรูปภาพแคนวาส และเราก็พร้อมกลับไปที่โค้ดแล้ว เราจะเริ่มต้นด้วยบรรทัดเดียวเพื่อให้เข้าใจง่าย เราจะเริ่มต้นด้วยการวาดเส้นทางจากปลายทางด้านซ้ายบนไปยังปลายทางด้านขวาล่าง เมื่อรูปหกเหลี่ยมก่อนหน้านี้มีขนาด 400x346 พิกเซล ปลายทางด้านบนจะมีขนาด 150 พิกเซลในแนวนอนและ 0 พิกเซลในแนวตั้ง โดยใช้อักษรย่อ (150, 0) จุดควบคุมของรูปภาพนี้คือ (150, 86) ปลายทางของขอบด้านล่างคือ (250, 346) โดยมีจุดควบคุมอยู่ที่ (250, 260)

พิกัดของเส้นโค้ง Bezier เส้นแรก
พิกัดสำหรับเส้นโค้ง Bezier แรก

เมื่อทราบพิกัดแล้ว เราก็พร้อมที่จะเริ่มวาด เราจะเริ่มต้นใหม่ด้วย ctx.beginPath() แล้วย้ายไปยังปลายทางแรกโดยใช้

ctx.moveTo(pointX1,pointY1);

จากนั้นวาดเส้นโดยใช้ ctx.bezierCurveTo() ดังนี้

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

เนื่องจากเราต้องการให้เส้นมีเส้นขอบที่สวยงาม เราจะวาดเส้นขอบของเส้นทางนี้ 2 ครั้งโดยใช้ความกว้างและสีที่แตกต่างกันในแต่ละครั้ง ระบบจะกำหนดสีโดยใช้พร็อพเพอร์ตี้ 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 จุด รวมถึงจุดควบคุมของเส้นโค้ง Bezier ที่เกี่ยวข้อง เราจะทำตามขั้นตอนด้านบนซ้ำและอาจสร้างชิ้นส่วนแผนที่ประมาณนี้

กระเบื้องหกเหลี่ยมที่เสร็จสมบูรณ์
การ์ดหกเหลี่ยมที่เสร็จสมบูรณ์

การหมุนภาพ

เมื่อเรามีชิ้นส่วนแล้ว เราต้องการที่จะหมุนชิ้นส่วนเพื่อให้ผู้เล่นเดินไปตามเส้นทางต่างๆ ในเกมได้ เราใช้ ctx.translate() และ ctx.rotate() เพื่อดำเนินการนี้โดยใช้ Canvas เราต้องการให้การ์ดหมุนรอบจุดศูนย์กลาง ดังนั้นขั้นตอนแรกคือการย้ายจุดอ้างอิงบนผืนผ้าใบไปยังจุดศูนย์กลางของการ์ดหกเหลี่ยม โดยเราใช้ข้อมูลต่อไปนี้

ctx.translate(originX, originY);

โดยที่ originX จะเป็นครึ่งหนึ่งของความกว้างของกระเบื้องหกเหลี่ยม และ originY จะเป็นครึ่งของความสูง สูตรที่ได้จะเป็นดังนี้

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

ตอนนี้เราหมุนการ์ดด้วยจุดศูนย์กลางใหม่ได้แล้ว เนื่องจากหกเหลี่ยมมี 6 ด้าน เราจึงต้องหมุนด้วยจำนวนที่คูณด้วย 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 ซึ่งรวมถึงการแสดงผลรูปภาพ การวาดเส้นโค้ง Bezier และการหมุนภาพ การใช้แท็ก 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();