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

เกริ่นนำ

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

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

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

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

ภาพต่อเรียงของชิ้นส่วน
ภาพต่อเรียงของไทล์

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

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

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

ฉันได้ตั้งรหัสเพื่อให้เราดึงรหัสลงในสคริปต์ได้

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

ต่อมาคือเราต้องจับบริบทแบบ 2 มิติของผืนผ้าใบเพื่อเริ่มวาดสิ่งต่างๆ ดังนี้

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

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

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

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

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 ของขอบให้ห่างจากกัน เช่น

ปลายเส้นบนชิ้นส่วนหกเหลี่ยม
จุดสิ้นสุดของเส้นบนชิ้นส่วนหกเหลี่ยม

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

จุดควบคุมบนไทล์หกเหลี่ยม
จุดควบคุมบนชิ้นส่วนหกเหลี่ยม

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

เนื่องจากเราต้องการให้เส้นมีเส้นขอบสวยสบาย เราจึงจะลากเส้นเส้นทางนี้ 2 ครั้งโดยใช้ความกว้างและสีที่แตกต่างกันในแต่ละครั้ง ระบบจะตั้งค่าสีโดยใช้คุณสมบัติ ctx.stageStyle และความกว้างโดยใช้ 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() เพื่อทำเช่นนี้โดยใช้ Canvas เราต้องการให้ชิ้นส่วนหมุนประมาณจุดกึ่งกลาง ขั้นตอนแรกคือการย้ายจุดอ้างอิงของ 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);

การวางการแปลและการหมุนด้านบนก่อนโค้ดการแสดงผลของเราจะทำให้โค้ดแสดงผล แบบหมุนได้

ไทล์หกเหลี่ยมที่หมุน
ไทล์หกเหลี่ยมหมุน

สรุป

ด้านบนผมได้ไฮไลต์ความสามารถ 2-3 อย่างที่ HTML5 มีให้ ด้วยการใช้แท็ก Canvas ซึ่งรวมถึงการแสดงภาพ การวาดเส้นโค้งเบซิเยร์ และการหมุน 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();