HTML5 – Canvas: Di chuyển đối tượng bằng bàn phím

preferences-desktop-keyboard-shortcutsBàn phím là thiết bị không thể thiếu và là phương tiện rất quan trọng để thực hiện các chức năng của các ứng dụng tương tác với người dùng. Trong bài viết này, tôi sẽ hướng dẫn cách bắt sự kiện bàn phím trong canvas và dùng nó để điều khiển góc xoay và hướng di chuyển của đối tượng đồ họa.

Xem Demo.

Bắt sự kiện bàn phím

Để bắt sự kiện này, bạn có thể đăng kí trực tiếp cho đối tượng window, tuy nhiên điều này sẽ gây ra những rắc rối khi trang của bạn có quá nhiều thành phần. Cách tốt nhất là đăng kí riêng cho canvas các hàm xử lý. Tuy nhiên, để canvas có thể focus được, bạn cần xác định thuộc tính TabIndex của nó:

<canvas id=”canvas” width=”300″ height=”200″

tabindex=”1″ style=”border: 1px solid;”></canvas>

Sau đó bạn đăng kí các sự kiện cần thiết cho canvas. Khi chạy trên trình duyệt, bạn cần phải click chuột vào canvas để nó nhận được focus trước khi thực hiện các thao tác bàn phím.

_canvas.onkeydown = canvas_keyDown;

function canvas_keyDown(e){
	alert(e.keyCode);
}

Tham số event truyền vào hàm xử lý sẽ chứa các thuộc tính cần thiết để bạn xác định được phím nào được nhấn. Ở đây, bạn chỉ cần quan tâm đến thuộc tính keyCode lưu mã của phím được nhấn. Do các giá trị keyCode có kiểu số nên rất khó nhớ và kiểm tra, may mắn là tôi tìm được trang “Javascript Keycode Enums” liệt kê sẵn các keyCode dưới dạng enum của một đối tượng. Tôi sửa lại một chút để tiện sử dụng hơn:

var Keys = {
      BACKSPACE: 8,
      TAB: 9,
      ENTER: 13,
      SHIFT: 16,
      CTRL: 17,
      ALT: 18,
      PAUSE: 19,
      CAPS_LOCK: 20,
      ESCAPE: 27,
      SPACE: 32,
      PAGE_UP: 33,
      PAGE_DOWN: 34,
      END: 35,
      HOME: 36,
      LEFT_ARROW: 37,
      UP_ARROW: 38,
      RIGHT_ARROW: 39,
      DOWN_ARROW: 40,
      INSERT: 45,
      DELETE: 46,
      KEY_0: 48,
      KEY_1: 49,
      KEY_2: 50,
      KEY_3: 51,
      KEY_4: 52,
      KEY_5: 53,
      KEY_6: 54,
      KEY_7: 55,
      KEY_8: 56,
      KEY_9: 57,
      KEY_A: 65,
      KEY_B: 66,
      KEY_C: 67,
      KEY_D: 68,
      KEY_E: 69,
      KEY_F: 70,
      KEY_G: 71,
      KEY_H: 72,
      KEY_I: 73,
      KEY_J: 74,
      KEY_K: 75,
      KEY_L: 76,
      KEY_M: 77,
      KEY_N: 78,
      KEY_O: 79,
      KEY_P: 80,
      KEY_Q: 81,
      KEY_R: 82,
      KEY_S: 83,
      KEY_T: 84,
      KEY_U: 85,
      KEY_V: 86,
      KEY_W: 87,
      KEY_X: 88,
      KEY_Y: 89,
      KEY_Z: 90,
      LEFT_META: 91,
      RIGHT_META: 92,
      SELECT: 93,
      NUMPAD_0: 96,
      NUMPAD_1: 97,
      NUMPAD_2: 98,
      NUMPAD_3: 99,
      NUMPAD_4: 100,
      NUMPAD_5: 101,
      NUMPAD_6: 102,
      NUMPAD_7: 103,
      NUMPAD_8: 104,
      NUMPAD_9: 105,
      MULTIPLY: 106,
      ADD: 107,
      SUBTRACT: 109,
      DECIMAL: 110,
      DIVIDE: 111,
      F1: 112,
      F2: 113,
      F3: 114,
      F4: 115,
      F5: 116,
      F6: 117,
      F7: 118,
      F8: 119,
      F9: 120,
      F10: 121,
      F11: 122,
      F12: 123,
      NUM_LOCK: 144,
      SCROLL_LOCK: 145,
      SEMICOLON: 186,
      EQUALS: 187,
      COMMA: 188,
      DASH: 189,
      PERIOD: 190,
      FORWARD_SLASH: 191,
      GRAVE_ACCENT: 192,
      OPEN_BRACKET: 219,
      BACK_SLASH: 220,
      CLOSE_BRACKET: 221,
      SINGLE_QUOTE: 222
    };

Kiểm tra trạng thái của nhiều phím

Một khó khăn của các sự kiện bàn phím trong javascript là chỉ có thể xác định được duy nhất một phím nhấn thông qua thuộc tính event.keyCode. Để có thể kiểm tra được trạng thái của nhiều phím được nhấn cùng lúc, ta phải lưu trạng thái của chúng lại khi sự kiện keyDown xảy ra và  xóa trạng thái đó đi trong sự kiện keyUp.

var _keypressed = {};
function canvas_keyDown(e){
	_keypressed[e.keyCode] = true;
}
function canvas_keyUp(e){
	_keypressed[e.keyCode] = false;
}

Giới hạn các phím được bắt

Sử dụng phương pháp trên, bạn cần lọc các phím cần sử dụng để đối tượng _keypressed không lưu các giá trị không cần thiết. Một cách đơn giản nhất là sử dụng mảng để lưu keyCode của các phím này và kiểm tra trong các sự kiện bàn phím:

const AVAILABLE_KEYS =
	[ 	Keys.UP_ARROW,
		Keys.DOWN_ARROW,
		Keys.LEFT_ARROW,
		Keys.RIGHT_ARROW
	];
function canvas_keyDown(e){
	if(AVAILABLE_KEYS.indexOf(e.keyCode)!=-1)
	{
		_keypressed[e.keyCode] = true;
	}
}
function canvas_keyUp(e){
	if(_keypressed[e.keyCode])
	{
		_keypressed[e.keyCode] = false;
	}
}

Ví dụ: Di chuyển xe tăng

Trong ví dụ này tôi sẽ tạo một lớp Tank và vẽ theo hình dạng một chiếc xe tăng đơn giản. Tại trang html, tôi sẽ bắt các sự kiện keyDown và keyUp để di chuyển đối tượng này theo 8 tám hướng

Xem Demo.

HTML5 - canvas - keyboard event

function Tank(mapWidth, mapHeight){
	this.mapWidth = mapWidth;
	this.mapHeight = mapHeight;
	this.radius = 20;
	this.speedX = 0;
	this.speedY = 0;
	this.power = 3;
	this.cx = mapWidth/2;
	this.cy = mapHeight/2;
}
Tank.prototype.draw = function(context){
	context.beginPath();
	context.fillStyle = "green";
	context.arc(this.cx,this.cy,this.radius,0,Math.PI*2,true);
	context.closePath();
	context.fill();

	// gun
	var size = this.radius/2;
	var angle = Math.atan2(this.speedY,this.speedX);;

	context.beginPath();
	context.fillStyle = "red";
	context.rect(this.cx,this.cy - size/2,size*3,size);
	context.closePath();
	context.fill();

	// yellow circle
	context.beginPath();
	context.fillStyle = "yellow";
	context.arc(this.cx,this.cy,this.radius/2,0,Math.PI*2,true);
	context.closePath();
	context.fill();

}

Tank.prototype.move = function(){
	this.cx += this.speedX;
	this.cy += this.speedY;

	this.left = this.cx - this.radius;
	this.top = this.cy - this.radius;
	this.right = this.cx + this.radius;
	this.bottom = this.cy + this.radius;
}
Tank.prototype.resetSpeed = function(){
	this.speedX = 0;
	this.speedY = 0;
}
Tank.prototype.moveUp = function(){
	this.speedY = -this.power;
}
Tank.prototype.moveDown = function(){
	this.speedY = this.power;
}
Tank.prototype.moveLeft = function(){
	this.speedX = -this.power;
}
Tank.prototype.moveRight = function(){
	this.speedX = this.power;
}

Xoay đối tượng trong canvas

Một vấn đề khi bạn di chuyển xe tăng là súng không thay đổi hướng và góc xoay cho phù hợp. Nhưng điều này giải quyết rất đơn giản, bởi vì đối tượng context của canvas hỗ trợ phương thức translate()rotate() dùng để dịch chuyển và xoay canvas. Như vậy, tôi sẽ thực hiện dịch chuyển tâm xoay đến giữa đối tượng (xe tăng) và gọi rotate() trước khi vẽ nòng súng ra canvas.

Để lấy được góc xoay, ta biết được hai giá trị speedX và speedY chính là hướng di chuyển của đối tượng. Hai giá trị này tạo thành hai cạnh kề của một tam giác vuông và góc của súng cũng chính là góc α được tính bằng công thức tan(α) = speedY/speedX.

Tan Angle

Như vậy ta có:

tan(α) = speedY/speedX.

=>  α = atan(speedY/speedX)

hoặc atan2(speedY,speedX)

Như vậy ta sẽ vẽ súng tại điểm gốc tọa độ là chính giữa xe tăng. Bạn nên lưu lại trạng thái của context với phương thức save() và gọi restore() để phục hồi lại trạng thái ban đầu cho nó khi thực hiện thay đổi các thông số của nó.

var angle = Math.atan2(this.speedY,this.speedX)
context.save();
context.translate(this.cx,this.cy);
context.rotate(angle);
context.beginPath();
context.fillStyle = "red";
context.rect(0,-size/2,size*3,size);
context.closePath();
context.fill();
context.restore();

Xem Demo.

 HTML5 - canvas - rotate shape

Mã nguồn hoàn chỉnh

Tank.js:

function Tank(mapWidth, mapHeight){
	this.mapWidth = mapWidth;
	this.mapHeight = mapHeight;
	this.radius = 20;
	this.speedX = 0;
	this.speedY = 0;
	this.power = 3;
	this.cx = mapWidth/2;
	this.cy = mapHeight/2;
}
Tank.prototype.draw = function(context){
	context.beginPath();
	context.fillStyle = "green";
	context.arc(this.cx,this.cy,this.radius,0,Math.PI*2,true);
	context.closePath();
	context.fill();

	// red rectangle
	var size = this.radius/2;
	var angle = Math.atan2(this.speedY,this.speedX);

	context.save();
	context.translate(this.cx,this.cy);
	context.rotate(angle);
	context.beginPath();
	context.fillStyle = "red";
	context.rect(0,-size/2,size*3,size);
	context.closePath();
	context.fill();
	context.restore();

	// yellow circle
	context.beginPath();
	context.fillStyle = "yellow";
	context.arc(this.cx,this.cy,this.radius/2,0,Math.PI*2,true);
	context.closePath();
	context.fill();

}

Tank.prototype.move = function(){
	this.cx += this.speedX;
	this.cy += this.speedY;

	this.left = this.cx - this.radius;
	this.top = this.cy - this.radius;
	this.right = this.cx + this.radius;
	this.bottom = this.cy + this.radius;
}
Tank.prototype.resetSpeed = function(){
	this.speedX = 0;
	this.speedY = 0;
}
Tank.prototype.moveUp = function(){
	this.speedY = -this.power;
}
Tank.prototype.moveDown = function(){
	this.speedY = this.power;
}
Tank.prototype.moveLeft = function(){
	this.speedX = -this.power;
}
Tank.prototype.moveRight = function(){
	this.speedX = this.power;
}

Sample.html:

<html>
<head>
<script src="tank.js"></script>
<script src="keys.js"></script>
<script type="text/javascript">
const AVAILABLE_KEYS =
	[ 	Keys.UP_ARROW,
		Keys.DOWN_ARROW,
		Keys.LEFT_ARROW,
		Keys.RIGHT_ARROW
	];

var _canvas;
var _context;
var _ball;
var _keypressed = {};

function clear() {
	_context.clearRect(0, 0, _canvas.width, _canvas.height);
}

function init() {
	_canvas = document.getElementById("canvas");
	_context = _canvas.getContext("2d");
	_ball = new Tank(_canvas.width,_canvas.height);
	draw();
}

function canvas_keyDown(e){
	if(AVAILABLE_KEYS.indexOf(e.keyCode)!=-1)
	{
		_keypressed[e.keyCode] = true;
		doKeypress();
	}
}
function canvas_keyUp(e){
	if(_keypressed[e.keyCode])
	{
		_keypressed[e.keyCode] = false;
		_ball.resetSpeed();
	}
}

function doKeypress(){
	if(_keypressed[Keys.UP_ARROW])
		_ball.moveUp();
	if(_keypressed[Keys.DOWN_ARROW])
		_ball.moveDown();
	if(_keypressed[Keys.LEFT_ARROW])
		_ball.moveLeft();
	if(_keypressed[Keys.RIGHT_ARROW])
		_ball.moveRight();
	_ball.move();
	draw();
}
function draw() {
	clear();
	_ball.draw(_context);
}
window.onload = function(){
	init();
_canvas.onkeydown = canvas_keyDown;
_canvas.onkeyup = canvas_keyUp;
}
</script>
</head>
<body>

<canvas id="canvas" width="300" height="200"
	tabindex="1" style="border: 1px solid;"></canvas>
</body>
</html>

YinYang’s Programming Blog

Advertisements

13 thoughts on “HTML5 – Canvas: Di chuyển đối tượng bằng bàn phím

  1. Topic thực sự rất cool! Dù là một HTML5 trainer (tự nhận ^^) anh cũng không thể chuẩn bị được các ví dụ như em. Game 2D hoặc những thứ liên quan tới đồ thị, biểu đồ.. chính là điều mà HTML5 được hướng đến để thay thế Flash. Silverlight với dòng ứng dụng dành cho enterprise vẫn có chỗ đứng. Vẫn theo dõi bài viết của em từng ngày ^^

    Phản hồi
  2. Cảm ơn anh! Sở thích của em là làm game nhưng chưa có điều kiện và khó có thể theo đuổi được. Nhân tiện học HTML5 nên tập trung làm thử mấy game đơn giản. Mấy hôm nay công việc của em hơi bận nên lười viết bài. Hy vọng được anh nhận xét và ủng hộ.

    Phản hồi
  3. xin chào anh!!! em đang làm đề tài tốt nghiệp điều khiển robot. Mới tìm hiểu về c# .E đang muốn thực hiện việc tạo 1 giao diện giống như game tank của Anh với sự tương tác trực tiếp với robot. Điều khiển robot thông qua giao diện điều khiển xe tank vậy đó. E mong anh chỉ e cách nhanh nhất để tìm hiểu.thank!

    Phản hồi
  4. Pingback: HTML5 – Canvas: Game xe tăng đơn giản – part 1 | Rai Information

  5. Chào anh!. Em đang tìm hiểu HTML5, em thấy bài viết của anh rất hay.
    Em có xem phần vẽ súng và có thắc mắc a: context.rect(0,-size/2,size*3,size);
    Tọa độ: 0,-size/2 thì sao lại là ở chỗ tầm giữa màn hình đấy đc a. Mong anh giải thích thêm đoạn này giúp e.
    Em cảm ơn!

    Phản hồi
  6. Pingback: C# – Kiểm tra tổ hợp phím, phân biệt KeyCode, KeyValue và KeyData | NGỌC TẤN KIẾN THỨC LẬP TRÌNH AN GIANG

Trả lời

Mời bạn điền thông tin vào ô dưới đây hoặc kích vào một biểu tượng để đăng nhập:

WordPress.com Logo

Bạn đang bình luận bằng tài khoản WordPress.com Log Out / Thay đổi )

Twitter picture

Bạn đang bình luận bằng tài khoản Twitter Log Out / Thay đổi )

Facebook photo

Bạn đang bình luận bằng tài khoản Facebook Log Out / Thay đổi )

Google+ photo

Bạn đang bình luận bằng tài khoản Google+ Log Out / Thay đổi )

Connecting to %s